Page MenuHomestyx hydra

No OneTemporary

diff --git a/src/applications/herald/controller/HeraldRuleController.php b/src/applications/herald/controller/HeraldRuleController.php
index c16a45b47e..6780693031 100644
--- a/src/applications/herald/controller/HeraldRuleController.php
+++ b/src/applications/herald/controller/HeraldRuleController.php
@@ -1,672 +1,675 @@
<?php
final class HeraldRuleController extends HeraldController {
private $id;
private $filter;
public function willProcessRequest(array $data) {
$this->id = (int)idx($data, 'id');
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$content_type_map = HeraldAdapter::getEnabledAdapterMap($user);
$rule_type_map = HeraldRuleTypeConfig::getRuleTypeMap();
if ($this->id) {
$id = $this->id;
$rule = id(new HeraldRuleQuery())
->setViewer($user)
->withIDs(array($id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$rule) {
return new Aphront404Response();
}
$cancel_uri = $this->getApplicationURI("rule/{$id}/");
} else {
$rule = new HeraldRule();
$rule->setAuthorPHID($user->getPHID());
$rule->setMustMatchAll(1);
$content_type = $request->getStr('content_type');
$rule->setContentType($content_type);
$rule_type = $request->getStr('rule_type');
if (!isset($rule_type_map[$rule_type])) {
$rule_type = HeraldRuleTypeConfig::RULE_TYPE_PERSONAL;
}
$rule->setRuleType($rule_type);
$adapter = HeraldAdapter::getAdapterForContentType(
$rule->getContentType());
if (!$adapter->supportsRuleType($rule->getRuleType())) {
throw new Exception(
pht(
"This rule's content type does not support the selected rule ".
"type."));
}
if ($rule->isObjectRule()) {
$rule->setTriggerObjectPHID($request->getStr('targetPHID'));
$object = id(new PhabricatorObjectQuery())
->setViewer($user)
->withPHIDs(array($rule->getTriggerObjectPHID()))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$object) {
throw new Exception(
pht('No valid object provided for object rule!'));
}
if (!$adapter->canTriggerOnObject($object)) {
throw new Exception(
pht('Object is of wrong type for adapter!'));
}
}
$cancel_uri = $this->getApplicationURI();
}
if ($rule->isGlobalRule()) {
$this->requireApplicationCapability(
HeraldManageGlobalRulesCapability::CAPABILITY);
}
$adapter = HeraldAdapter::getAdapterForContentType($rule->getContentType());
$local_version = id(new HeraldRule())->getConfigVersion();
if ($rule->getConfigVersion() > $local_version) {
throw new Exception(
pht(
'This rule was created with a newer version of Herald. You can not '.
'view or edit it in this older version. Upgrade your Phabricator '.
'deployment.'));
}
// Upgrade rule version to our version, since we might add newly-defined
// conditions, etc.
$rule->setConfigVersion($local_version);
$rule_conditions = $rule->loadConditions();
$rule_actions = $rule->loadActions();
$rule->attachConditions($rule_conditions);
$rule->attachActions($rule_actions);
$e_name = true;
$errors = array();
if ($request->isFormPost() && $request->getStr('save')) {
list($e_name, $errors) = $this->saveRule($adapter, $rule, $request);
if (!$errors) {
$id = $rule->getID();
$uri = $this->getApplicationURI("rule/{$id}/");
return id(new AphrontRedirectResponse())->setURI($uri);
}
}
$must_match_selector = $this->renderMustMatchSelector($rule);
$repetition_selector = $this->renderRepetitionSelector($rule, $adapter);
$handles = $this->loadHandlesForRule($rule);
require_celerity_resource('herald-css');
$content_type_name = $content_type_map[$rule->getContentType()];
$rule_type_name = $rule_type_map[$rule->getRuleType()];
$form = id(new AphrontFormView())
->setUser($user)
->setID('herald-rule-edit-form')
->addHiddenInput('content_type', $rule->getContentType())
->addHiddenInput('rule_type', $rule->getRuleType())
->addHiddenInput('save', 1)
->appendChild(
// Build this explicitly (instead of using addHiddenInput())
// so we can add a sigil to it.
javelin_tag(
'input',
array(
'type' => 'hidden',
'name' => 'rule',
'sigil' => 'rule',
)))
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Rule Name'))
->setName('name')
->setError($e_name)
->setValue($rule->getName()));
$trigger_object_control = false;
if ($rule->isObjectRule()) {
$trigger_object_control = id(new AphrontFormStaticControl())
->setValue(
pht(
'This rule triggers for %s.',
$handles[$rule->getTriggerObjectPHID()]->renderLink()));
}
$form
->appendChild(
id(new AphrontFormMarkupControl())
->setValue(pht(
'This %s rule triggers for %s.',
phutil_tag('strong', array(), $rule_type_name),
phutil_tag('strong', array(), $content_type_name))))
->appendChild($trigger_object_control)
->appendChild(
id(new PHUIFormInsetView())
->setTitle(pht('Conditions'))
->setRightButton(javelin_tag(
'a',
array(
'href' => '#',
'class' => 'button green',
'sigil' => 'create-condition',
'mustcapture' => true,
),
pht('New Condition')))
->setDescription(
pht('When %s these conditions are met:', $must_match_selector))
->setContent(javelin_tag(
'table',
array(
'sigil' => 'rule-conditions',
'class' => 'herald-condition-table',
),
'')))
->appendChild(
id(new PHUIFormInsetView())
->setTitle(pht('Action'))
->setRightButton(javelin_tag(
'a',
array(
'href' => '#',
'class' => 'button green',
'sigil' => 'create-action',
'mustcapture' => true,
),
pht('New Action')))
->setDescription(pht(
'Take these actions %s this rule matches:',
$repetition_selector))
->setContent(javelin_tag(
'table',
array(
'sigil' => 'rule-actions',
'class' => 'herald-action-table',
),
'')))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Save Rule'))
->addCancelButton($cancel_uri));
$this->setupEditorBehavior($rule, $handles, $adapter);
$title = $rule->getID()
? pht('Edit Herald Rule')
: pht('Create Herald Rule');
$form_box = id(new PHUIObjectBoxView())
->setHeaderText($title)
->setFormErrors($errors)
->setForm($form);
$crumbs = $this
->buildApplicationCrumbs()
->addTextCrumb($title);
return $this->buildApplicationPage(
array(
$crumbs,
$form_box,
),
array(
'title' => pht('Edit Rule'),
));
}
private function saveRule(HeraldAdapter $adapter, $rule, $request) {
$rule->setName($request->getStr('name'));
$match_all = ($request->getStr('must_match') == 'all');
$rule->setMustMatchAll((int)$match_all);
$repetition_policy_param = $request->getStr('repetition_policy');
$rule->setRepetitionPolicy(
HeraldRepetitionPolicyConfig::toInt($repetition_policy_param));
$e_name = true;
$errors = array();
if (!strlen($rule->getName())) {
$e_name = pht('Required');
$errors[] = pht('Rule must have a name.');
}
$data = json_decode($request->getStr('rule'), true);
if (!is_array($data) ||
!$data['conditions'] ||
!$data['actions']) {
throw new Exception('Failed to decode rule data.');
}
$conditions = array();
foreach ($data['conditions'] as $condition) {
if ($condition === null) {
// We manage this as a sparse array on the client, so may receive
// NULL if conditions have been removed.
continue;
}
$obj = new HeraldCondition();
$obj->setFieldName($condition[0]);
$obj->setFieldCondition($condition[1]);
if (is_array($condition[2])) {
$obj->setValue(array_keys($condition[2]));
} else {
$obj->setValue($condition[2]);
}
try {
$adapter->willSaveCondition($obj);
} catch (HeraldInvalidConditionException $ex) {
$errors[] = $ex->getMessage();
}
$conditions[] = $obj;
}
$actions = array();
foreach ($data['actions'] as $action) {
if ($action === null) {
// Sparse on the client; removals can give us NULLs.
continue;
}
if (!isset($action[1])) {
// Legitimate for any action which doesn't need a target, like
// "Do nothing".
$action[1] = null;
}
$obj = new HeraldAction();
$obj->setAction($action[0]);
$obj->setTarget($action[1]);
try {
$adapter->willSaveAction($rule, $obj);
} catch (HeraldInvalidActionException $ex) {
$errors[] = $ex;
}
$actions[] = $obj;
}
$rule->attachConditions($conditions);
$rule->attachActions($actions);
if (!$errors) {
$edit_action = $rule->getID() ? 'edit' : 'create';
$rule->openTransaction();
$rule->save();
$rule->saveConditions($conditions);
$rule->saveActions($actions);
$rule->saveTransaction();
}
return array($e_name, $errors);
}
private function setupEditorBehavior(
HeraldRule $rule,
array $handles,
HeraldAdapter $adapter) {
$serial_conditions = array(
array('default', 'default', ''),
);
if ($rule->getConditions()) {
$serial_conditions = array();
foreach ($rule->getConditions() as $condition) {
$value = $condition->getValue();
switch ($condition->getFieldName()) {
case HeraldAdapter::FIELD_TASK_PRIORITY:
$value_map = array();
$priority_map = ManiphestTaskPriority::getTaskPriorityMap();
foreach ($value as $priority) {
$value_map[$priority] = idx($priority_map, $priority);
}
$value = $value_map;
break;
case HeraldAdapter::FIELD_TASK_STATUS:
$value_map = array();
$status_map = ManiphestTaskStatus::getTaskStatusMap();
foreach ($value as $status) {
$value_map[$status] = idx($status_map, $status);
}
$value = $value_map;
break;
default:
if (is_array($value)) {
$value_map = array();
foreach ($value as $k => $fbid) {
$value_map[$fbid] = $handles[$fbid]->getName();
}
$value = $value_map;
}
break;
}
$serial_conditions[] = array(
$condition->getFieldName(),
$condition->getFieldCondition(),
$value,
);
}
}
$serial_actions = array(
array('default', ''),
);
if ($rule->getActions()) {
$serial_actions = array();
foreach ($rule->getActions() as $action) {
switch ($action->getAction()) {
case HeraldAdapter::ACTION_FLAG:
case HeraldAdapter::ACTION_BLOCK:
$current_value = $action->getTarget();
break;
default:
if (is_array($action->getTarget())) {
$target_map = array();
foreach ((array)$action->getTarget() as $fbid) {
$target_map[$fbid] = $handles[$fbid]->getName();
}
$current_value = $target_map;
} else {
$current_value = $action->getTarget();
}
break;
}
$serial_actions[] = array(
$action->getAction(),
$current_value,
);
}
}
$all_rules = $this->loadRulesThisRuleMayDependUpon($rule);
$all_rules = mpull($all_rules, 'getName', 'getPHID');
asort($all_rules);
$all_fields = $adapter->getFieldNameMap();
$all_conditions = $adapter->getConditionNameMap();
$all_actions = $adapter->getActionNameMap($rule->getRuleType());
$fields = $adapter->getFields();
$field_map = array_select_keys($all_fields, $fields);
// Populate any fields which exist in the rule but which we don't know the
// names of, so that saving a rule without touching anything doesn't change
// it.
foreach ($rule->getConditions() as $condition) {
if (empty($field_map[$condition->getFieldName()])) {
$field_map[$condition->getFieldName()] = pht('<Unknown Field>');
}
}
$actions = $adapter->getActions($rule->getRuleType());
$action_map = array_select_keys($all_actions, $actions);
$config_info = array();
$config_info['fields'] = $field_map;
$config_info['conditions'] = $all_conditions;
$config_info['actions'] = $action_map;
foreach ($config_info['fields'] as $field => $name) {
$field_conditions = $adapter->getConditionsForField($field);
$config_info['conditionMap'][$field] = $field_conditions;
}
foreach ($config_info['fields'] as $field => $fname) {
foreach ($config_info['conditionMap'][$field] as $condition) {
$value_type = $adapter->getValueTypeForFieldAndCondition(
$field,
$condition);
$config_info['values'][$field][$condition] = $value_type;
}
}
$config_info['rule_type'] = $rule->getRuleType();
foreach ($config_info['actions'] as $action => $name) {
$config_info['targets'][$action] = $adapter->getValueTypeForAction(
$action,
$rule->getRuleType());
}
$changeflag_options =
PhabricatorRepositoryPushLog::getHeraldChangeFlagConditionOptions();
Javelin::initBehavior(
'herald-rule-editor',
array(
'root' => 'herald-rule-edit-form',
'conditions' => (object)$serial_conditions,
'actions' => (object)$serial_actions,
'select' => array(
HeraldAdapter::VALUE_CONTENT_SOURCE => array(
'options' => PhabricatorContentSource::getSourceNameMap(),
'default' => PhabricatorContentSource::SOURCE_WEB,
),
HeraldAdapter::VALUE_FLAG_COLOR => array(
'options' => PhabricatorFlagColor::getColorNameMap(),
'default' => PhabricatorFlagColor::COLOR_BLUE,
),
HeraldPreCommitRefAdapter::VALUE_REF_TYPE => array(
'options' => array(
PhabricatorRepositoryPushLog::REFTYPE_BRANCH
=> pht('branch (git/hg)'),
PhabricatorRepositoryPushLog::REFTYPE_TAG
=> pht('tag (git)'),
PhabricatorRepositoryPushLog::REFTYPE_BOOKMARK
=> pht('bookmark (hg)'),
),
'default' => PhabricatorRepositoryPushLog::REFTYPE_BRANCH,
),
HeraldPreCommitRefAdapter::VALUE_REF_CHANGE => array(
'options' => $changeflag_options,
'default' => PhabricatorRepositoryPushLog::CHANGEFLAG_ADD,
),
),
'template' => $this->buildTokenizerTemplates($handles) + array(
'rules' => $all_rules,
),
'author' => array(
$rule->getAuthorPHID() =>
$handles[$rule->getAuthorPHID()]->getName(),
),
'info' => $config_info,
));
}
private function loadHandlesForRule($rule) {
$phids = array();
foreach ($rule->getActions() as $action) {
if (!is_array($action->getTarget())) {
continue;
}
foreach ($action->getTarget() as $target) {
$target = (array)$target;
foreach ($target as $phid) {
$phids[] = $phid;
}
}
}
foreach ($rule->getConditions() as $condition) {
$value = $condition->getValue();
if (is_array($value)) {
foreach ($value as $phid) {
$phids[] = $phid;
}
}
}
$phids[] = $rule->getAuthorPHID();
if ($rule->isObjectRule()) {
$phids[] = $rule->getTriggerObjectPHID();
}
return $this->loadViewerHandles($phids);
}
/**
* Render the selector for the "When (all of | any of) these conditions are
* met:" element.
*/
private function renderMustMatchSelector($rule) {
return AphrontFormSelectControl::renderSelectTag(
$rule->getMustMatchAll() ? 'all' : 'any',
array(
'all' => pht('all of'),
'any' => pht('any of'),
),
array(
'name' => 'must_match',
));
}
/**
* Render the selector for "Take these actions (every time | only the first
* time) this rule matches..." element.
*/
private function renderRepetitionSelector($rule, HeraldAdapter $adapter) {
$repetition_policy = HeraldRepetitionPolicyConfig::toString(
$rule->getRepetitionPolicy());
$repetition_options = $adapter->getRepetitionOptions();
$repetition_names = HeraldRepetitionPolicyConfig::getMap();
$repetition_map = array_select_keys($repetition_names, $repetition_options);
if (count($repetition_map) < 2) {
return head($repetition_names);
} else {
return AphrontFormSelectControl::renderSelectTag(
$repetition_policy,
$repetition_map,
array(
'name' => 'repetition_policy',
));
}
}
protected function buildTokenizerTemplates(array $handles) {
$template = new AphrontTokenizerTemplateView();
$template = $template->render();
$sources = array(
'repository' => new DiffusionRepositoryDatasource(),
'legaldocuments' => new LegalpadDocumentDatasource(),
'taskpriority' => new ManiphestTaskPriorityDatasource(),
'taskstatus' => new ManiphestTaskStatusDatasource(),
'buildplan' => new HarbormasterBuildPlanDatasource(),
'arcanistprojects' => new DiffusionArcanistProjectDatasource(),
'package' => new PhabricatorOwnersPackageDatasource(),
'project' => new PhabricatorProjectDatasource(),
'user' => new PhabricatorPeopleDatasource(),
'email' => new PhabricatorMetaMTAMailableDatasource(),
'userorproject' => new PhabricatorProjectOrUserDatasource(),
'applicationemail' => new PhabricatorMetaMTAApplicationEmailDatasource(),
);
foreach ($sources as $key => $source) {
+ $source->setViewer($this->getViewer());
+
$sources[$key] = array(
'uri' => $source->getDatasourceURI(),
'placeholder' => $source->getPlaceholderText(),
+ 'browseURI' => $source->getBrowseURI(),
);
}
return array(
'source' => $sources,
'username' => $this->getRequest()->getUser()->getUserName(),
'icons' => mpull($handles, 'getTypeIcon', 'getPHID'),
'markup' => $template,
);
}
/**
* Load rules for the "Another Herald rule..." condition dropdown, which
* allows one rule to depend upon the success or failure of another rule.
*/
private function loadRulesThisRuleMayDependUpon(HeraldRule $rule) {
$viewer = $this->getRequest()->getUser();
// Any rule can depend on a global rule.
$all_rules = id(new HeraldRuleQuery())
->setViewer($viewer)
->withRuleTypes(array(HeraldRuleTypeConfig::RULE_TYPE_GLOBAL))
->withContentTypes(array($rule->getContentType()))
->execute();
if ($rule->isObjectRule()) {
// Object rules may depend on other rules for the same object.
$all_rules += id(new HeraldRuleQuery())
->setViewer($viewer)
->withRuleTypes(array(HeraldRuleTypeConfig::RULE_TYPE_OBJECT))
->withContentTypes(array($rule->getContentType()))
->withTriggerObjectPHIDs(array($rule->getTriggerObjectPHID()))
->execute();
}
if ($rule->isPersonalRule()) {
// Personal rules may depend upon your other personal rules.
$all_rules += id(new HeraldRuleQuery())
->setViewer($viewer)
->withRuleTypes(array(HeraldRuleTypeConfig::RULE_TYPE_PERSONAL))
->withContentTypes(array($rule->getContentType()))
->withAuthorPHIDs(array($rule->getAuthorPHID()))
->execute();
}
// mark disabled rules as disabled since they are not useful as such;
// don't filter though to keep edit cases sane / expected
foreach ($all_rules as $current_rule) {
if ($current_rule->getIsDisabled()) {
$current_rule->makeEphemeral();
$current_rule->setName($rule->getName().' '.pht('(Disabled)'));
}
}
// A rule can not depend upon itself.
unset($all_rules[$rule->getID()]);
return $all_rules;
}
}
diff --git a/src/applications/mailinglists/typeahead/PhabricatorMailingListDatasource.php b/src/applications/mailinglists/typeahead/PhabricatorMailingListDatasource.php
index c08da2a30d..b9435e0b4a 100644
--- a/src/applications/mailinglists/typeahead/PhabricatorMailingListDatasource.php
+++ b/src/applications/mailinglists/typeahead/PhabricatorMailingListDatasource.php
@@ -1,38 +1,35 @@
<?php
final class PhabricatorMailingListDatasource
extends PhabricatorTypeaheadDatasource {
- public function isBrowsable() {
- // TODO: Make this browsable if we don't delete it before then.
- return false;
- }
-
public function getPlaceholderText() {
return pht('Type a mailing list name...');
}
public function getDatasourceApplicationClass() {
return 'PhabricatorMailingListsApplication';
}
public function loadResults() {
$viewer = $this->getViewer();
$raw_query = $this->getRawQuery();
- $results = array();
+ $query = id(new PhabricatorMailingListQuery());
+ $lists = $this->executeQuery($query);
- $lists = id(new PhabricatorMailingListQuery())
- ->setViewer($viewer)
- ->execute();
+ $results = array();
foreach ($lists as $list) {
$results[] = id(new PhabricatorTypeaheadResult())
->setName($list->getName())
->setURI($list->getURI())
->setPHID($list->getPHID());
}
- return $results;
+ // TODO: It would be slightly preferable to do this as part of the query,
+ // this is just simpler for the moment.
+
+ return $this->filterResultsAgainstTokens($results);
}
}
diff --git a/src/applications/maniphest/controller/ManiphestBatchEditController.php b/src/applications/maniphest/controller/ManiphestBatchEditController.php
index f22269c10e..f604d58d0c 100644
--- a/src/applications/maniphest/controller/ManiphestBatchEditController.php
+++ b/src/applications/maniphest/controller/ManiphestBatchEditController.php
@@ -1,379 +1,384 @@
<?php
final class ManiphestBatchEditController extends ManiphestController {
public function processRequest() {
$this->requireApplicationCapability(
ManiphestBulkEditCapability::CAPABILITY);
$request = $this->getRequest();
$user = $request->getUser();
$task_ids = $request->getArr('batch');
$tasks = id(new ManiphestTaskQuery())
->setViewer($user)
->withIDs($task_ids)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->needSubscriberPHIDs(true)
->needProjectPHIDs(true)
->execute();
$actions = $request->getStr('actions');
if ($actions) {
$actions = json_decode($actions, true);
}
if ($request->isFormPost() && is_array($actions)) {
foreach ($tasks as $task) {
$field_list = PhabricatorCustomField::getObjectFields(
$task,
PhabricatorCustomField::ROLE_EDIT);
$field_list->readFieldsFromStorage($task);
$xactions = $this->buildTransactions($actions, $task);
if ($xactions) {
// TODO: Set content source to "batch edit".
$editor = id(new ManiphestTransactionEditor())
->setActor($user)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->applyTransactions($task, $xactions);
}
}
$task_ids = implode(',', mpull($tasks, 'getID'));
return id(new AphrontRedirectResponse())
->setURI('/maniphest/?ids='.$task_ids);
}
$handles = ManiphestTaskListView::loadTaskHandles($user, $tasks);
$list = new ManiphestTaskListView();
$list->setTasks($tasks);
$list->setUser($user);
$list->setHandles($handles);
$template = new AphrontTokenizerTemplateView();
$template = $template->render();
$projects_source = new PhabricatorProjectDatasource();
$mailable_source = new PhabricatorMetaMTAMailableDatasource();
+ $mailable_source->setViewer($user);
$owner_source = new PhabricatorTypeaheadOwnerDatasource();
+ $owner_source->setViewer($user);
require_celerity_resource('maniphest-batch-editor');
Javelin::initBehavior(
'maniphest-batch-editor',
array(
'root' => 'maniphest-batch-edit-form',
'tokenizerTemplate' => $template,
'sources' => array(
'project' => array(
- 'src' => $projects_source->getDatasourceURI(),
- 'placeholder' => $projects_source->getPlaceholderText(),
+ 'src' => $projects_source->getDatasourceURI(),
+ 'placeholder' => $projects_source->getPlaceholderText(),
+ 'browseURI' => $projects_source->getBrowseURI(),
),
'owner' => array(
- 'src' => $owner_source->getDatasourceURI(),
- 'placeholder' => $owner_source->getPlaceholderText(),
- 'limit' => 1,
+ 'src' => $owner_source->getDatasourceURI(),
+ 'placeholder' => $owner_source->getPlaceholderText(),
+ 'browseURI' => $owner_source->getBrowseURI(),
+ 'limit' => 1,
),
- 'cc' => array(
- 'src' => $mailable_source->getDatasourceURI(),
- 'placeholder' => $mailable_source->getPlaceholderText(),
+ 'cc' => array(
+ 'src' => $mailable_source->getDatasourceURI(),
+ 'placeholder' => $mailable_source->getPlaceholderText(),
+ 'browseURI' => $mailable_source->getBrowseURI(),
),
),
'input' => 'batch-form-actions',
'priorityMap' => ManiphestTaskPriority::getTaskPriorityMap(),
'statusMap' => ManiphestTaskStatus::getTaskStatusMap(),
));
$form = new AphrontFormView();
$form->setUser($user);
$form->setID('maniphest-batch-edit-form');
foreach ($tasks as $task) {
$form->appendChild(
phutil_tag(
'input',
array(
'type' => 'hidden',
'name' => 'batch[]',
'value' => $task->getID(),
)));
}
$form->appendChild(
phutil_tag(
'input',
array(
'type' => 'hidden',
'name' => 'actions',
'id' => 'batch-form-actions',
)));
$form->appendChild(
id(new PHUIFormInsetView())
->setTitle(pht('Actions'))
->setRightButton(javelin_tag(
'a',
array(
'href' => '#',
'class' => 'button green',
'sigil' => 'add-action',
'mustcapture' => true,
),
pht('Add Another Action')))
->setContent(javelin_tag(
'table',
array(
'sigil' => 'maniphest-batch-actions',
'class' => 'maniphest-batch-actions-table',
),
'')))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Update Tasks'))
->addCancelButton('/maniphest/'));
$title = pht('Batch Editor');
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb($title);
$task_box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Selected Tasks'))
->appendChild($list);
$form_box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Batch Editor'))
->setForm($form);
return $this->buildApplicationPage(
array(
$crumbs,
$task_box,
$form_box,
),
array(
'title' => $title,
));
}
private function buildTransactions($actions, ManiphestTask $task) {
$value_map = array();
$type_map = array(
'add_comment' => PhabricatorTransactions::TYPE_COMMENT,
'assign' => ManiphestTransaction::TYPE_OWNER,
'status' => ManiphestTransaction::TYPE_STATUS,
'priority' => ManiphestTransaction::TYPE_PRIORITY,
'add_project' => PhabricatorTransactions::TYPE_EDGE,
'remove_project' => PhabricatorTransactions::TYPE_EDGE,
'add_ccs' => PhabricatorTransactions::TYPE_SUBSCRIBERS,
'remove_ccs' => PhabricatorTransactions::TYPE_SUBSCRIBERS,
);
$edge_edit_types = array(
'add_project' => true,
'remove_project' => true,
'add_ccs' => true,
'remove_ccs' => true,
);
$xactions = array();
foreach ($actions as $action) {
if (empty($type_map[$action['action']])) {
throw new Exception("Unknown batch edit action '{$action}'!");
}
$type = $type_map[$action['action']];
// Figure out the current value, possibly after modifications by other
// batch actions of the same type. For example, if the user chooses to
// "Add Comment" twice, we should add both comments. More notably, if the
// user chooses "Remove Project..." and also "Add Project...", we should
// avoid restoring the removed project in the second transaction.
if (array_key_exists($type, $value_map)) {
$current = $value_map[$type];
} else {
switch ($type) {
case PhabricatorTransactions::TYPE_COMMENT:
$current = null;
break;
case ManiphestTransaction::TYPE_OWNER:
$current = $task->getOwnerPHID();
break;
case ManiphestTransaction::TYPE_STATUS:
$current = $task->getStatus();
break;
case ManiphestTransaction::TYPE_PRIORITY:
$current = $task->getPriority();
break;
case PhabricatorTransactions::TYPE_EDGE:
$current = $task->getProjectPHIDs();
break;
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
$current = $task->getSubscriberPHIDs();
break;
}
}
// Check if the value is meaningful / provided, and normalize it if
// necessary. This discards, e.g., empty comments and empty owner
// changes.
$value = $action['value'];
switch ($type) {
case PhabricatorTransactions::TYPE_COMMENT:
if (!strlen($value)) {
continue 2;
}
break;
case ManiphestTransaction::TYPE_OWNER:
if (empty($value)) {
continue 2;
}
$value = head($value);
if ($value === ManiphestTaskOwner::OWNER_UP_FOR_GRABS) {
$value = null;
}
break;
case PhabricatorTransactions::TYPE_EDGE:
if (empty($value)) {
continue 2;
}
break;
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
if (empty($value)) {
continue 2;
}
break;
}
// If the edit doesn't change anything, go to the next action. This
// check is only valid for changes like "owner", "status", etc, not
// for edge edits, because we should still apply an edit like
// "Remove Projects: A, B" to a task with projects "A, B".
if (empty($edge_edit_types[$action['action']])) {
if ($value == $current) {
continue;
}
}
// Apply the value change; for most edits this is just replacement, but
// some need to merge the current and edited values (add/remove project).
switch ($type) {
case PhabricatorTransactions::TYPE_COMMENT:
if (strlen($current)) {
$value = $current."\n\n".$value;
}
break;
case PhabricatorTransactions::TYPE_EDGE:
$is_remove = $action['action'] == 'remove_project';
$current = array_fill_keys($current, true);
$value = array_fill_keys($value, true);
$new = $current;
$did_something = false;
if ($is_remove) {
foreach ($value as $phid => $ignored) {
if (isset($new[$phid])) {
unset($new[$phid]);
$did_something = true;
}
}
} else {
foreach ($value as $phid => $ignored) {
if (empty($new[$phid])) {
$new[$phid] = true;
$did_something = true;
}
}
}
if (!$did_something) {
continue 2;
}
$value = array_keys($new);
break;
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
$is_remove = $action['action'] == 'remove_ccs';
$current = array_fill_keys($current, true);
$new = array();
$did_something = false;
if ($is_remove) {
foreach ($value as $phid) {
if (isset($current[$phid])) {
$new[$phid] = true;
$did_something = true;
}
}
if ($new) {
$value = array('-' => array_keys($new));
}
} else {
$new = array();
foreach ($value as $phid) {
$new[$phid] = true;
$did_something = true;
}
if ($new) {
$value = array('+' => array_keys($new));
}
}
if (!$did_something) {
continue 2;
}
break;
}
$value_map[$type] = $value;
}
$template = new ManiphestTransaction();
foreach ($value_map as $type => $value) {
$xaction = clone $template;
$xaction->setTransactionType($type);
switch ($type) {
case PhabricatorTransactions::TYPE_COMMENT:
$xaction->attachComment(
id(new ManiphestTransactionComment())
->setContent($value));
break;
case PhabricatorTransactions::TYPE_EDGE:
$project_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST;
$xaction
->setMetadataValue('edge:type', $project_type)
->setNewValue(
array(
'=' => array_fuse($value),
));
break;
default:
$xaction->setNewValue($value);
break;
}
$xactions[] = $xaction;
}
return $xactions;
}
}
diff --git a/src/applications/policy/rule/PhabricatorLegalpadSignaturePolicyRule.php b/src/applications/policy/rule/PhabricatorLegalpadSignaturePolicyRule.php
index 14d207b4d8..9797d5ef80 100644
--- a/src/applications/policy/rule/PhabricatorLegalpadSignaturePolicyRule.php
+++ b/src/applications/policy/rule/PhabricatorLegalpadSignaturePolicyRule.php
@@ -1,74 +1,68 @@
<?php
final class PhabricatorLegalpadSignaturePolicyRule
extends PhabricatorPolicyRule {
private $signatures = array();
public function getRuleDescription() {
return pht('signers of legalpad documents');
}
public function willApplyRules(PhabricatorUser $viewer, array $values) {
$values = array_unique(array_filter(array_mergev($values)));
if (!$values) {
return;
}
// TODO: This accepts signature of any version of the document, even an
// older version.
$documents = id(new LegalpadDocumentQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs($values)
->withSignerPHIDs(array($viewer->getPHID()))
->execute();
$this->signatures = mpull($documents, 'getPHID', 'getPHID');
}
public function applyRule(PhabricatorUser $viewer, $value) {
foreach ($value as $document_phid) {
if (!isset($this->signatures[$document_phid])) {
return false;
}
}
return true;
}
public function getValueControlType() {
return self::CONTROL_TYPE_TOKENIZER;
}
public function getValueControlTemplate() {
- $datasource = new LegalpadDocumentDatasource();
-
- return array(
- 'markup' => new AphrontTokenizerTemplateView(),
- 'uri' => $datasource->getDatasourceURI(),
- 'placeholder' => $datasource->getPlaceholderText(),
- );
+ return $this->getDatasourceTemplate(new LegalpadDocumentDatasource());
}
public function getRuleOrder() {
return 900;
}
public function getValueForStorage($value) {
PhutilTypeSpec::newFromString('list<string>')->check($value);
return array_values($value);
}
public function getValueForDisplay(PhabricatorUser $viewer, $value) {
$handles = id(new PhabricatorHandleQuery())
->setViewer($viewer)
->withPHIDs($value)
->execute();
return mpull($handles, 'getFullName', 'getPHID');
}
public function ruleHasEffect($value) {
return (bool)$value;
}
}
diff --git a/src/applications/policy/rule/PhabricatorPolicyRule.php b/src/applications/policy/rule/PhabricatorPolicyRule.php
index 6540d797df..eaa04e1e9e 100644
--- a/src/applications/policy/rule/PhabricatorPolicyRule.php
+++ b/src/applications/policy/rule/PhabricatorPolicyRule.php
@@ -1,70 +1,80 @@
<?php
abstract class PhabricatorPolicyRule {
const CONTROL_TYPE_TEXT = 'text';
const CONTROL_TYPE_SELECT = 'select';
const CONTROL_TYPE_TOKENIZER = 'tokenizer';
const CONTROL_TYPE_NONE = 'none';
abstract public function getRuleDescription();
abstract public function applyRule(PhabricatorUser $viewer, $value);
public function willApplyRules(PhabricatorUser $viewer, array $values) {
return;
}
public function getValueControlType() {
return self::CONTROL_TYPE_TEXT;
}
public function getValueControlTemplate() {
return null;
}
+ protected function getDatasourceTemplate(
+ PhabricatorTypeaheadDatasource $datasource) {
+ return array(
+ 'markup' => new AphrontTokenizerTemplateView(),
+ 'uri' => $datasource->getDatasourceURI(),
+ 'placeholder' => $datasource->getPlaceholderText(),
+ 'browseURI' => $datasource->getBrowseURI(),
+ );
+ }
+
public function getRuleOrder() {
return 500;
}
public function getValueForStorage($value) {
return $value;
}
public function getValueForDisplay(PhabricatorUser $viewer, $value) {
return $value;
}
public function getRequiredHandlePHIDsForSummary($value) {
$phids = array();
switch ($this->getValueControlType()) {
case self::CONTROL_TYPE_TOKENIZER:
$phids = $value;
break;
case self::CONTROL_TYPE_TEXT:
case self::CONTROL_TYPE_SELECT:
case self::CONTROL_TYPE_NONE:
default:
if (phid_get_type($value) !=
PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) {
$phids = array($value);
} else {
$phids = array();
}
break;
}
return $phids;
}
/**
* Return true if the given value creates a rule with a meaningful effect.
* An example of a rule with no meaningful effect is a "users" rule with no
* users specified.
*
* @return bool True if the value creates a meaningful rule.
*/
public function ruleHasEffect($value) {
return true;
}
}
diff --git a/src/applications/policy/rule/PhabricatorProjectsPolicyRule.php b/src/applications/policy/rule/PhabricatorProjectsPolicyRule.php
index 7bd75495c8..eca97ce37d 100644
--- a/src/applications/policy/rule/PhabricatorProjectsPolicyRule.php
+++ b/src/applications/policy/rule/PhabricatorProjectsPolicyRule.php
@@ -1,72 +1,66 @@
<?php
final class PhabricatorProjectsPolicyRule extends PhabricatorPolicyRule {
private $memberships = array();
public function getRuleDescription() {
return pht('members of projects');
}
public function willApplyRules(PhabricatorUser $viewer, array $values) {
$values = array_unique(array_filter(array_mergev($values)));
if (!$values) {
return;
}
$projects = id(new PhabricatorProjectQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withMemberPHIDs(array($viewer->getPHID()))
->withPHIDs($values)
->execute();
foreach ($projects as $project) {
$this->memberships[$viewer->getPHID()][$project->getPHID()] = true;
}
}
public function applyRule(PhabricatorUser $viewer, $value) {
foreach ($value as $project_phid) {
if (isset($this->memberships[$viewer->getPHID()][$project_phid])) {
return true;
}
}
return false;
}
public function getValueControlType() {
return self::CONTROL_TYPE_TOKENIZER;
}
public function getValueControlTemplate() {
- $projects_source = new PhabricatorProjectDatasource();
-
- return array(
- 'markup' => new AphrontTokenizerTemplateView(),
- 'uri' => $projects_source->getDatasourceURI(),
- 'placeholder' => $projects_source->getPlaceholderText(),
- );
+ return $this->getDatasourceTemplate(new PhabricatorProjectDatasource());
}
public function getRuleOrder() {
return 200;
}
public function getValueForStorage($value) {
PhutilTypeSpec::newFromString('list<string>')->check($value);
return array_values($value);
}
public function getValueForDisplay(PhabricatorUser $viewer, $value) {
$handles = id(new PhabricatorHandleQuery())
->setViewer($viewer)
->withPHIDs($value)
->execute();
return mpull($handles, 'getFullName', 'getPHID');
}
public function ruleHasEffect($value) {
return (bool)$value;
}
}
diff --git a/src/applications/policy/rule/PhabricatorUsersPolicyRule.php b/src/applications/policy/rule/PhabricatorUsersPolicyRule.php
index 03be81e064..c60e43abb6 100644
--- a/src/applications/policy/rule/PhabricatorUsersPolicyRule.php
+++ b/src/applications/policy/rule/PhabricatorUsersPolicyRule.php
@@ -1,58 +1,52 @@
<?php
final class PhabricatorUsersPolicyRule extends PhabricatorPolicyRule {
public function getRuleDescription() {
return pht('users');
}
public function applyRule(PhabricatorUser $viewer, $value) {
foreach ($value as $phid) {
if ($phid == $viewer->getPHID()) {
return true;
}
}
return false;
}
public function getValueControlType() {
return self::CONTROL_TYPE_TOKENIZER;
}
public function getValueControlTemplate() {
- $users_datasource = new PhabricatorPeopleDatasource();
-
- return array(
- 'markup' => new AphrontTokenizerTemplateView(),
- 'uri' => $users_datasource->getDatasourceURI(),
- 'placeholder' => $users_datasource->getPlaceholderText(),
- );
+ return $this->getDatasourceTemplate(new PhabricatorPeopleDatasource());
}
public function getRuleOrder() {
return 100;
}
public function getValueForStorage($value) {
PhutilTypeSpec::newFromString('list<string>')->check($value);
return array_values($value);
}
public function getValueForDisplay(PhabricatorUser $viewer, $value) {
if (!$value) {
return array();
}
$handles = id(new PhabricatorHandleQuery())
->setViewer($viewer)
->withPHIDs($value)
->execute();
return mpull($handles, 'getFullName', 'getPHID');
}
public function ruleHasEffect($value) {
return (bool)$value;
}
}
diff --git a/src/view/control/AphrontTokenizerTemplateView.php b/src/view/control/AphrontTokenizerTemplateView.php
index 009f3e134e..f0e34f2b1e 100644
--- a/src/view/control/AphrontTokenizerTemplateView.php
+++ b/src/view/control/AphrontTokenizerTemplateView.php
@@ -1,131 +1,136 @@
<?php
final class AphrontTokenizerTemplateView extends AphrontView {
private $value;
private $name;
private $id;
private $browseURI;
public function setBrowseURI($browse_uri) {
$this->browseURI = $browse_uri;
return $this;
}
public function setID($id) {
$this->id = $id;
return $this;
}
public function setValue(array $value) {
assert_instances_of($value, 'PhabricatorObjectHandle');
$this->value = $value;
return $this;
}
public function getValue() {
return $this->value;
}
public function setName($name) {
$this->name = $name;
return $this;
}
public function getName() {
return $this->name;
}
public function render() {
require_celerity_resource('aphront-tokenizer-control-css');
$id = $this->id;
$name = $this->getName();
$values = nonempty($this->getValue(), array());
$tokens = array();
foreach ($values as $key => $value) {
$tokens[] = $this->renderToken(
$value->getPHID(),
$value->getFullName(),
$value->getType());
}
$input = javelin_tag(
'input',
array(
'mustcapture' => true,
'name' => $name,
'class' => 'jx-tokenizer-input',
'sigil' => 'tokenizer-input',
'style' => 'width: 0px;',
'disabled' => 'disabled',
'type' => 'text',
));
$content = $tokens;
$content[] = $input;
$content[] = phutil_tag('div', array('style' => 'clear: both;'), '');
- $container = phutil_tag(
+ $container = javelin_tag(
'div',
array(
'id' => $id,
'class' => 'jx-tokenizer-container',
+ 'sigil' => 'tokenizer-container',
),
$content);
- $browse = null;
+ $icon = id(new PHUIIconView())
+ ->setIconFont('fa-list-ul');
+
+ // TODO: This thing is ugly and the ugliness is not intentional.
+ // We have to give it text or PHUIButtonView collapses. It should likely
+ // just be an icon and look more integrated into the input.
+ $browse = id(new PHUIButtonView())
+ ->setTag('a')
+ ->setIcon($icon)
+ ->addSigil('tokenizer-browse')
+ ->setColor(PHUIButtonView::GREY)
+ ->setSize(PHUIButtonView::SMALL)
+ ->setText(pht('Browse...'));
+
+ $classes = array();
+ $classes[] = 'jx-tokenizer-frame';
+
if ($this->browseURI) {
- $icon = id(new PHUIIconView())
- ->setIconFont('fa-list-ul');
-
- // TODO: This thing is ugly and the ugliness is not intentional.
- // We have to give it text or PHUIButtonView collapses. It should likely
- // just be an icon and look more integrated into the input.
- $browse = id(new PHUIButtonView())
- ->setTag('a')
- ->setIcon($icon)
- ->addSigil('tokenizer-browse')
- ->setColor(PHUIButtonView::GREY)
- ->setSize(PHUIButtonView::SMALL)
- ->setText(pht('Browse...'));
+ $classes[] = 'has-browse';
}
$frame = javelin_tag(
'table',
array(
- 'class' => 'jx-tokenizer-frame',
+ 'class' => implode(' ', $classes),
'sigil' => 'tokenizer-frame',
),
phutil_tag(
'tr',
array(
),
array(
phutil_tag(
'td',
array(
'class' => 'jx-tokenizer-frame-input',
),
$container),
phutil_tag(
'td',
array(
'class' => 'jx-tokenizer-frame-browse',
),
$browse),
)));
return $frame;
}
private function renderToken($key, $value, $icon) {
return id(new PhabricatorTypeaheadTokenView())
->setKey($key)
->setValue($value)
->setIcon($icon)
->setInputName($this->getName());
}
}
diff --git a/webroot/rsrc/css/aphront/tokenizer.css b/webroot/rsrc/css/aphront/tokenizer.css
index 074f54e5d8..2056c88c31 100644
--- a/webroot/rsrc/css/aphront/tokenizer.css
+++ b/webroot/rsrc/css/aphront/tokenizer.css
@@ -1,120 +1,128 @@
/**
* @provides aphront-tokenizer-control-css
* @requires aphront-typeahead-control-css
*/
body div.jx-tokenizer {
background: transparent;
position: relative;
width: 100%;
}
body div.jx-tokenizer-container {
position: relative;
display: block;
padding: 0 0 2px 0;
min-height: 30px;
height: auto;
}
var.jx-tokenizer-metrics {
position: absolute;
left: 20px;
top: 20px;
}
body input.jx-tokenizer-input {
border: 1px solid transparent;
border-width: 1px 0px;
padding: 3px;
outline: none;
float: left;
width: 100%;
border-shadow: none;
box-shadow: none;
-webkit-box-shadow: none;
font-size: 13px;
color: #333;
height: 26px;
}
body input.jx-tokenizer-input:focus {
box-shadow: none;
-webkit-box-shadow: none;
border-color: transparent;
}
body input.jx-typeahead-placeholder {
margin-left: 4px;
color: {$greytext};
}
a.jx-tokenizer-x {
margin-left: 4px;
color: {$bluetext};
}
a.jx-tokenizer-x:hover {
color: {$darkbluetext};
text-decoration: none;
}
a.jx-tokenizer-token {
padding: 2px 6px 3px;
border: 1px solid {$lightblueborder};
margin: 3px 2px 0 4px;
background: #dee7f8;
float: left;
cursor: pointer;
border-radius: 3px;
color: {$darkbluetext};
min-height: 16px;
}
a.jx-tokenizer-token:hover {
text-decoration: none;
border-color: {$blueborder};
background: #CDD9F0;
}
.jx-tokenizer-token .phui-icon-view {
display: inline-block;
margin: 2px 4px -3px 0;
color: {$bluetext};
}
.tokenizer-result {
position: relative;
padding: 5px 8px 5px 28px;
}
.tokenizer-result .phui-icon-view {
display: inline-block;
width: 24px;
height: 24px;
position: absolute;
top: 5px;
left: 8px;
}
.tokenizer-result-closed {
color: {$greytext};
}
.tokenizer-closed {
margin-top: 2px;
}
.jx-tokenizer-frame {
width: 100%;
}
-.jx-tokenizer-frame-input {
+.jx-tokenizer-frame .jx-tokenizer-frame-browse {
+ display: none;
+}
+
+.has-browse .jx-tokenizer-frame-browse {
+ display: table-cell;
+}
+
+.jx-tokenizer-frame td.jx-tokenizer-frame-input {
width: 100%;
}
.jx-tokenizer-frame-browse {
width: 100px;
vertical-align: middle;
padding: 0 0 0 4px;
}
diff --git a/webroot/rsrc/externals/javelin/lib/control/tokenizer/Tokenizer.js b/webroot/rsrc/externals/javelin/lib/control/tokenizer/Tokenizer.js
index bdde22fb88..93e540ece3 100644
--- a/webroot/rsrc/externals/javelin/lib/control/tokenizer/Tokenizer.js
+++ b/webroot/rsrc/externals/javelin/lib/control/tokenizer/Tokenizer.js
@@ -1,469 +1,470 @@
/**
* @requires javelin-dom
* javelin-util
* javelin-stratcom
* javelin-install
* @provides javelin-tokenizer
* @javelin
*/
/**
* A tokenizer is a UI component similar to a text input, except that it
* allows the user to input a list of items ("tokens"), generally from a fixed
* set of results. A familiar example of this UI is the "To:" field of most
* email clients, where the control autocompletes addresses from the user's
* address book.
*
* @{JX.Tokenizer} is built on top of @{JX.Typeahead}, and primarily adds the
* ability to choose multiple items.
*
* To build a @{JX.Tokenizer}, you need to do four things:
*
* 1. Construct it, padding a DOM node for it to attach to. See the constructor
* for more information.
* 2. Build a {@JX.Typeahead} and configure it with setTypeahead().
* 3. Configure any special options you want.
* 4. Call start().
*
* If you do this correctly, the input should suggest items and enter them as
* tokens as the user types.
*
* When the tokenizer is focused, the CSS class `jx-tokenizer-container-focused`
* is added to the container node.
*/
JX.install('Tokenizer', {
construct : function(containerNode) {
this._containerNode = containerNode;
},
events : [
/**
* Emitted when the value of the tokenizer changes, similar to an 'onchange'
* from a <select />.
*/
'change'],
properties : {
limit : null,
renderTokenCallback : null,
browseURI: null
},
members : {
_containerNode : null,
_root : null,
_frame: null,
_focus : null,
_orig : null,
_typeahead : null,
_tokenid : 0,
_tokens : null,
_tokenMap : null,
_initialValue : null,
_seq : 0,
_lastvalue : null,
_placeholder : null,
start : function() {
if (__DEV__) {
if (!this._typeahead) {
throw new Error(
'JX.Tokenizer.start(): ' +
'No typeahead configured! Use setTypeahead() to provide a ' +
'typeahead.');
}
}
this._orig = JX.DOM.find(this._containerNode, 'input', 'tokenizer-input');
this._tokens = [];
this._tokenMap = {};
try {
this._frame = JX.DOM.findAbove(this._orig, 'table', 'tokenizer-frame');
} catch (e) {
// Ignore, this tokenizer doesn't have a frame.
}
if (this._frame) {
+ JX.DOM.alterClass(this._frame, 'has-browse', !!this.getBrowseURI());
JX.DOM.listen(
this._frame,
'click',
'tokenizer-browse',
JX.bind(this, this._onbrowse));
}
var focus = this.buildInput(this._orig.value);
this._focus = focus;
var input_container = JX.DOM.scry(
this._containerNode,
'div',
'tokenizer-input-container'
);
input_container = input_container[0] || this._containerNode;
JX.DOM.listen(
focus,
['click', 'focus', 'blur', 'keydown', 'keypress', 'paste'],
null,
JX.bind(this, this.handleEvent));
// NOTE: Safari on the iPhone does not normally delegate click events on
// <div /> tags. This causes the event to fire. We want a click (in this
// case, a touch) anywhere in the div to trigger this event so that we
// can focus the input. Without this, you must tap an arbitrary area on
// the left side of the input to focus it.
//
// http://www.quirksmode.org/blog/archives/2010/09/click_event_del.html
input_container.onclick = JX.bag;
JX.DOM.listen(
input_container,
'click',
null,
JX.bind(
this,
function(e) {
if (e.getNode('remove')) {
this._remove(e.getNodeData('token').key, true);
} else if (e.getTarget() == this._root) {
this.focus();
}
}));
var root = JX.$N('div');
root.id = this._orig.id;
JX.DOM.alterClass(root, 'jx-tokenizer', true);
root.style.cursor = 'text';
this._root = root;
root.appendChild(focus);
var typeahead = this._typeahead;
typeahead.setInputNode(this._focus);
typeahead.start();
setTimeout(JX.bind(this, function() {
var container = this._orig.parentNode;
JX.DOM.setContent(container, root);
var map = this._initialValue || {};
for (var k in map) {
this.addToken(k, map[k]);
}
JX.DOM.appendContent(
root,
JX.$N('div', {style: {clear: 'both'}})
);
this._redraw();
}), 0);
},
setInitialValue : function(map) {
this._initialValue = map;
return this;
},
setTypeahead : function(typeahead) {
typeahead.setAllowNullSelection(false);
typeahead.removeListener();
typeahead.listen(
'choose',
JX.bind(this, function(result) {
JX.Stratcom.context().prevent();
if (this.addToken(result.rel, result.name)) {
if (this.shouldHideResultsOnChoose()) {
this._typeahead.hide();
}
this._typeahead.clear();
this._redraw();
this.focus();
}
})
);
typeahead.listen(
'query',
JX.bind(
this,
function(query) {
// TODO: We should emit a 'query' event here to allow the caller to
// generate tokens on the fly, e.g. email addresses or other freeform
// or algorithmic tokens.
// Then do this if something handles the event.
// this._focus.value = '';
// this._redraw();
// this.focus();
if (query.length) {
// Prevent this event if there's any text, so that we don't submit
// the form (either we created a token or we failed to create a
// token; in either case we shouldn't submit). If the query is
// empty, allow the event so that the form submission takes place.
JX.Stratcom.context().prevent();
}
}));
this._typeahead = typeahead;
return this;
},
shouldHideResultsOnChoose : function() {
return true;
},
handleEvent : function(e) {
this._typeahead.handleEvent(e);
if (e.getPrevented()) {
return;
}
if (e.getType() == 'click') {
if (e.getTarget() == this._root) {
this.focus();
e.prevent();
return;
}
} else if (e.getType() == 'keydown') {
this._onkeydown(e);
} else if (e.getType() == 'blur') {
this._didblur();
// Explicitly update the placeholder since we just wiped the field
// value.
this._typeahead.updatePlaceholder();
} else if (e.getType() == 'focus') {
this._didfocus();
} else if (e.getType() == 'paste') {
setTimeout(JX.bind(this, this._redraw), 0);
}
},
refresh : function() {
this._redraw(true);
return this;
},
_redraw : function(force) {
// If there are tokens in the tokenizer, never show a placeholder.
// Otherwise, show one if one is configured.
if (JX.keys(this._tokenMap).length) {
this._typeahead.setPlaceholder(null);
} else {
this._typeahead.setPlaceholder(this._placeholder);
}
var focus = this._focus;
if (focus.value === this._lastvalue && !force) {
return;
}
this._lastvalue = focus.value;
var metrics = JX.DOM.textMetrics(
this._focus,
'jx-tokenizer-metrics');
metrics.y = null;
metrics.x += 24;
metrics.setDim(focus);
// NOTE: Once, long ago, we set "focus.value = focus.value;" here to fix
// an issue with copy/paste in Firefox not redrawing correctly. However,
// this breaks input of Japanese glyphs in Chrome, and I can't reproduce
// the original issue in modern Firefox.
//
// If future changes muck around with things here, test that Japanese
// inputs still work. Example:
//
// - Switch to Hiragana mode.
// - Type "ni".
// - This should produce a glyph, not the value "n".
//
// With the assignment, Chrome loses the partial input on the "n" when
// the value is assigned.
},
setPlaceholder : function(string) {
this._placeholder = string;
return this;
},
addToken : function(key, value) {
if (key in this._tokenMap) {
return false;
}
var focus = this._focus;
var root = this._root;
var token = this.buildToken(key, value);
this._tokenMap[key] = {
value : value,
key : key,
node : token
};
this._tokens.push(key);
root.insertBefore(token, focus);
this.invoke('change', this);
return true;
},
removeToken : function(key) {
return this._remove(key, false);
},
buildInput: function(value) {
return JX.$N('input', {
className: 'jx-tokenizer-input',
type: 'text',
autocomplete: 'off',
value: value
});
},
/**
* Generate a token based on a key and value. The "token" and "remove"
* sigils are observed by a listener in start().
*/
buildToken: function(key, value) {
var input = JX.$N('input', {
type: 'hidden',
value: key,
name: this._orig.name + '[' + (this._seq++) + ']'
});
var remove = JX.$N('a', {
className: 'jx-tokenizer-x',
sigil: 'remove'
}, '\u00d7'); // U+00D7 multiplication sign
var display_token = value;
var render_callback = this.getRenderTokenCallback();
if (render_callback) {
display_token = render_callback(value, key);
}
return JX.$N('a', {
className: 'jx-tokenizer-token',
sigil: 'token',
meta: {key: key}
}, [display_token, input, remove]);
},
getTokens : function() {
var result = {};
for (var key in this._tokenMap) {
result[key] = this._tokenMap[key].value;
}
return result;
},
_onkeydown : function(e) {
var raw = e.getRawEvent();
if (raw.ctrlKey || raw.metaKey || raw.altKey) {
return;
}
switch (e.getSpecialKey()) {
case 'tab':
var completed = this._typeahead.submit();
if (!completed) {
this._focus.value = '';
}
break;
case 'delete':
if (!this._focus.value.length) {
var tok;
while ((tok = this._tokens.pop())) {
if (this._remove(tok, true)) {
break;
}
}
}
break;
case 'return':
// Don't subject this to token limits.
break;
default:
if (this.getLimit() &&
JX.keys(this._tokenMap).length == this.getLimit()) {
e.prevent();
}
setTimeout(JX.bind(this, this._redraw), 0);
break;
}
},
_remove : function(index, focus) {
if (!this._tokenMap[index]) {
return false;
}
JX.DOM.remove(this._tokenMap[index].node);
delete this._tokenMap[index];
this._redraw(true);
focus && this.focus();
this.invoke('change', this);
return true;
},
focus : function() {
var focus = this._focus;
JX.DOM.show(focus);
// NOTE: We must fire this focus event immediately (during event
// handling) for the iPhone to bring up the keyboard. Previously this
// focus was wrapped in setTimeout(), but it's unclear why that was
// necessary. If this is adjusted later, make sure tapping the inactive
// area of the tokenizer to focus it on the iPhone still brings up the
// keyboard.
JX.DOM.focus(focus);
},
_didfocus : function() {
JX.DOM.alterClass(
this._containerNode,
'jx-tokenizer-container-focused',
true);
},
_didblur : function() {
JX.DOM.alterClass(
this._containerNode,
'jx-tokenizer-container-focused',
false);
this._focus.value = '';
this._redraw();
},
_onbrowse: function(e) {
e.kill();
var uri = this.getBrowseURI();
if (!uri) {
return;
}
new JX.Workflow(uri, {exclude: JX.keys(this.getTokens()).join(',')})
.setHandler(
JX.bind(this, function(r) {
this._typeahead.getDatasource().addResult(r.token);
this.addToken(r.key);
this.focus();
}))
.start();
}
}
});
diff --git a/webroot/rsrc/js/application/herald/HeraldRuleEditor.js b/webroot/rsrc/js/application/herald/HeraldRuleEditor.js
index 36f28bc865..abf645ba29 100644
--- a/webroot/rsrc/js/application/herald/HeraldRuleEditor.js
+++ b/webroot/rsrc/js/application/herald/HeraldRuleEditor.js
@@ -1,377 +1,373 @@
/**
* @requires multirow-row-manager
* javelin-install
* javelin-util
* javelin-dom
* javelin-stratcom
* javelin-json
* phabricator-prefab
* @provides herald-rule-editor
* @javelin
*/
JX.install('HeraldRuleEditor', {
construct : function(config) {
var root = JX.$(config.root);
this._root = root;
JX.DOM.listen(
root,
'click',
'create-condition',
JX.bind(this, this._onnewcondition));
JX.DOM.listen(
root,
'click',
'create-action',
JX.bind(this, this._onnewaction));
JX.DOM.listen(root, 'change', null, JX.bind(this, this._onchange));
JX.DOM.listen(root, 'submit', null, JX.bind(this, this._onsubmit));
var conditionsTable = JX.DOM.find(root, 'table', 'rule-conditions');
var actionsTable = JX.DOM.find(root, 'table', 'rule-actions');
this._conditionsRowManager = new JX.MultirowRowManager(conditionsTable);
this._conditionsRowManager.listen(
'row-removed',
JX.bind(this, function(row_id) {
delete this._config.conditions[row_id];
}));
this._actionsRowManager = new JX.MultirowRowManager(actionsTable);
this._actionsRowManager.listen(
'row-removed',
JX.bind(this, function(row_id) {
delete this._config.actions[row_id];
}));
this._conditionGetters = {};
this._conditionTypes = {};
this._actionGetters = {};
this._actionTypes = {};
this._config = config;
var conditions = this._config.conditions;
this._config.conditions = [];
var actions = this._config.actions;
this._config.actions = [];
this._renderConditions(conditions);
this._renderActions(actions);
},
members : {
_config : null,
_root : null,
_conditionGetters : null,
_conditionTypes : null,
_actionGetters : null,
_actionTypes : null,
_conditionsRowManager : null,
_actionsRowManager : null,
_onnewcondition : function(e) {
this._newCondition();
e.kill();
},
_onnewaction : function(e) {
this._newAction();
e.kill();
},
_onchange : function(e) {
var target = e.getTarget();
var row = e.getNode(JX.MultirowRowManager.getRowSigil());
if (!row) {
// Changing the "when all of / any of these..." dropdown.
return;
}
if (JX.Stratcom.hasSigil(target, 'field-select')) {
this._onfieldchange(row);
} else if (JX.Stratcom.hasSigil(target, 'condition-select')) {
this._onconditionchange(row);
} else if (JX.Stratcom.hasSigil(target, 'action-select')) {
this._onactionchange(row);
}
},
_onsubmit : function() {
var rule = JX.DOM.find(this._root, 'input', 'rule');
var k;
for (k in this._config.conditions) {
this._config.conditions[k][2] = this._getConditionValue(k);
}
for (k in this._config.actions) {
this._config.actions[k][1] = this._getActionTarget(k);
}
rule.value = JX.JSON.stringify({
conditions: this._config.conditions,
actions: this._config.actions
});
},
_getConditionValue : function(id) {
if (this._conditionGetters[id]) {
return this._conditionGetters[id]();
}
return this._config.conditions[id][2];
},
_getActionTarget : function(id) {
if (this._actionGetters[id]) {
return this._actionGetters[id]();
}
return this._config.actions[id][1];
},
_onactionchange : function(r) {
var target = JX.DOM.find(r, 'select', 'action-select');
var row_id = this._actionsRowManager.getRowID(r);
this._config.actions[row_id][0] = target.value;
var target_cell = JX.DOM.find(r, 'td', 'target-cell');
var target_input = this._renderTargetInputForRow(row_id);
JX.DOM.setContent(target_cell, target_input);
},
_onfieldchange : function(r) {
var target = JX.DOM.find(r, 'select', 'field-select');
var row_id = this._actionsRowManager.getRowID(r);
this._config.conditions[row_id][0] = target.value;
var condition_cell = JX.DOM.find(r, 'td', 'condition-cell');
var condition_select = this._renderSelect(
this._selectKeys(
this._config.info.conditions,
this._config.info.conditionMap[target.value]),
this._config.conditions[row_id][1],
'condition-select');
JX.DOM.setContent(condition_cell, condition_select);
this._onconditionchange(r);
var condition_name = this._config.conditions[row_id][1];
if (condition_name == 'unconditionally') {
JX.DOM.hide(condition_select);
}
},
_onconditionchange : function(r) {
var target = JX.DOM.find(r, 'select', 'condition-select');
var row_id = this._conditionsRowManager.getRowID(r);
this._config.conditions[row_id][1] = target.value;
var value_cell = JX.DOM.find(r, 'td', 'value-cell');
var value_input = this._renderValueInputForRow(row_id);
JX.DOM.setContent(value_cell, value_input);
},
_renderTargetInputForRow : function(row_id) {
var action = this._config.actions[row_id];
var type = this._config.info.targets[action[0]];
var input = this._buildInput(type);
var node = input[0];
var get_fn = input[1];
var set_fn = input[2];
if (node) {
JX.Stratcom.addSigil(node, 'action-target');
}
var old_type = this._actionTypes[row_id];
if (old_type == type || !old_type) {
set_fn(this._getActionTarget(row_id));
}
this._actionTypes[row_id] = type;
this._actionGetters[row_id] = get_fn;
return node;
},
_buildInput : function(type) {
var input;
var get_fn;
var set_fn;
switch (type) {
case 'rule':
input = this._renderSelect(this._config.template.rules);
get_fn = function() { return input.value; };
set_fn = function(v) { input.value = v; };
break;
case 'email':
case 'user':
case 'repository':
case 'tag':
case 'package':
case 'project':
case 'userorproject':
case 'buildplan':
case 'taskpriority':
case 'taskstatus':
case 'arcanistprojects':
case 'legaldocuments':
case 'applicationemail':
var tokenizer = this._newTokenizer(type);
input = tokenizer[0];
get_fn = tokenizer[1];
set_fn = tokenizer[2];
break;
case 'none':
input = '';
get_fn = JX.bag;
set_fn = JX.bag;
break;
case 'contentsource':
case 'flagcolor':
case 'value-ref-type':
case 'value-ref-change':
input = this._renderSelect(this._config.select[type].options);
get_fn = function() { return input.value; };
set_fn = function(v) { input.value = v; };
set_fn(this._config.select[type]['default']);
break;
default:
input = JX.$N('input', {type: 'text'});
get_fn = function() { return input.value; };
set_fn = function(v) { input.value = v; };
break;
}
return [input, get_fn, set_fn];
},
_renderValueInputForRow : function(row_id) {
var cond = this._config.conditions[row_id];
var type = this._config.info.values[cond[0]][cond[1]];
var input = this._buildInput(type);
var node = input[0];
var get_fn = input[1];
var set_fn = input[2];
if (node) {
JX.Stratcom.addSigil(node, 'condition-value');
}
var old_type = this._conditionTypes[row_id];
if (old_type == type || !old_type) {
set_fn(this._getConditionValue(row_id));
}
this._conditionTypes[row_id] = type;
this._conditionGetters[row_id] = get_fn;
return node;
},
_newTokenizer : function(type) {
- var template = JX.$N(
- 'div',
- JX.$H(this._config.template.markup));
- template = template.firstChild;
- template.id = '';
-
var tokenizerConfig = {
- root : template,
src : this._config.template.source[type].uri,
placeholder: this._config.template.source[type].placeholder,
+ browseURI: this._config.template.source[type].browseURI,
icons : this._config.template.icons,
username : this._config.username
};
- var build = JX.Prefab.buildTokenizer(tokenizerConfig);
+ var build = JX.Prefab.newTokenizerFromTemplate(
+ this._config.template.markup,
+ tokenizerConfig);
build.tokenizer.start();
return [
- template,
+ build.node,
function() {
return build.tokenizer.getTokens();
},
function(map) {
for (var k in map) {
build.tokenizer.addToken(k, map[k]);
}
}];
},
_selectKeys : function(map, keys) {
var r = {};
for (var ii = 0; ii < keys.length; ii++) {
r[keys[ii]] = map[keys[ii]];
}
return r;
},
_renderConditions : function(conditions) {
for (var k in conditions) {
this._newCondition(conditions[k]);
}
},
_newCondition : function(data) {
var row = this._conditionsRowManager.addRow([]);
var row_id = this._conditionsRowManager.getRowID(row);
this._config.conditions[row_id] = data || [null, null, ''];
var r = this._conditionsRowManager.updateRow(
row_id,
this._renderCondition(row_id));
this._onfieldchange(r);
},
_renderCondition : function(row_id) {
var field_select = this._renderSelect(
this._config.info.fields,
this._config.conditions[row_id][0],
'field-select');
var field_cell = JX.$N('td', {sigil: 'field-cell'}, field_select);
var condition_cell = JX.$N('td', {sigil: 'condition-cell'});
var value_cell = JX.$N('td', {className : 'value', sigil: 'value-cell'});
return [field_cell, condition_cell, value_cell];
},
_renderActions : function(actions) {
for (var k in actions) {
this._newAction(actions[k]);
delete actions[k];
}
},
_newAction : function(data) {
data = data || [];
var temprow = this._actionsRowManager.addRow([]);
var row_id = this._actionsRowManager.getRowID(temprow);
this._config.actions[row_id] = data;
var r = this._actionsRowManager.updateRow(row_id,
this._renderAction(data));
this._onactionchange(r);
},
_renderAction : function(action) {
var action_select = this._renderSelect(
this._config.info.actions,
action[0],
'action-select');
var action_cell = JX.$N('td', {sigil: 'action-cell'}, action_select);
var target_cell = JX.$N(
'td',
{className : 'target', sigil : 'target-cell'});
return [action_cell, target_cell];
},
_renderSelect : function(map, selected, sigil) {
var attrs = {
sigil : sigil
};
return JX.Prefab.renderSelect(map, selected, attrs);
}
}
});
diff --git a/webroot/rsrc/js/application/maniphest/behavior-batch-editor.js b/webroot/rsrc/js/application/maniphest/behavior-batch-editor.js
index 6b7768615f..5cf568ce2c 100644
--- a/webroot/rsrc/js/application/maniphest/behavior-batch-editor.js
+++ b/webroot/rsrc/js/application/maniphest/behavior-batch-editor.js
@@ -1,154 +1,150 @@
/**
* @provides javelin-behavior-maniphest-batch-editor
* @requires javelin-behavior
* javelin-dom
* javelin-util
* phabricator-prefab
* multirow-row-manager
* javelin-json
*/
JX.behavior('maniphest-batch-editor', function(config) {
var root = JX.$(config.root);
var editor_table = JX.DOM.find(root, 'table', 'maniphest-batch-actions');
var manager = new JX.MultirowRowManager(editor_table);
var action_rows = [];
function renderRow() {
var action_select = JX.Prefab.renderSelect(
{
'add_project': 'Add Projects',
'remove_project' : 'Remove Projects',
'priority': 'Change Priority',
'status': 'Change Status',
'add_comment': 'Comment',
'assign': 'Assign',
'add_ccs' : 'Add CCs',
'remove_ccs' : 'Remove CCs'
});
var proj_tokenizer = build_tokenizer(config.sources.project);
var owner_tokenizer = build_tokenizer(config.sources.owner);
var cc_tokenizer = build_tokenizer(config.sources.cc);
var priority_select = JX.Prefab.renderSelect(config.priorityMap);
var status_select = JX.Prefab.renderSelect(config.statusMap);
var comment_input = JX.$N('input', {style: {width: '100%'}});
var cell = JX.$N('td', {className: 'batch-editor-input'});
var vfunc = null;
function update() {
switch (action_select.value) {
case 'add_project':
case 'remove_project':
JX.DOM.setContent(cell, proj_tokenizer.template);
vfunc = function() {
return JX.keys(proj_tokenizer.object.getTokens());
};
break;
case 'add_ccs':
case 'remove_ccs':
JX.DOM.setContent(cell, cc_tokenizer.template);
vfunc = function() {
return JX.keys(cc_tokenizer.object.getTokens());
};
break;
case 'assign':
JX.DOM.setContent(cell, owner_tokenizer.template);
vfunc = function() {
return JX.keys(owner_tokenizer.object.getTokens());
};
break;
case 'add_comment':
JX.DOM.setContent(cell, comment_input);
vfunc = function() {
return comment_input.value;
};
break;
case 'priority':
JX.DOM.setContent(cell, priority_select);
vfunc = function() { return priority_select.value; };
break;
case 'status':
JX.DOM.setContent(cell, status_select);
vfunc = function() { return status_select.value; };
break;
}
}
JX.DOM.listen(action_select, 'change', null, update);
update();
return {
nodes : [JX.$N('td', {}, action_select), cell],
dataCallback : function() {
return {
action: action_select.value,
value: vfunc()
};
}
};
}
function onaddaction(e) {
e.kill();
addRow({});
}
function addRow(info) {
var data = renderRow(info);
var row = manager.addRow(data.nodes);
var id = manager.getRowID(row);
action_rows[id] = data.dataCallback;
}
function onsubmit() {
var input = JX.$(config.input);
var actions = [];
for (var k in action_rows) {
actions.push(action_rows[k]());
}
input.value = JX.JSON.stringify(actions);
}
addRow({});
JX.DOM.listen(
root,
'click',
'add-action',
onaddaction);
JX.DOM.listen(
root,
'submit',
null,
onsubmit);
manager.listen(
'row-removed',
function(row_id) {
delete action_rows[row_id];
});
function build_tokenizer(tconfig) {
- var template = JX.$N('div', JX.$H(config.tokenizerTemplate)).firstChild;
- template.id = '';
-
- var build_config = JX.copy({}, tconfig);
- build_config.root = template;
-
- var built = JX.Prefab.buildTokenizer(build_config);
+ var built = JX.Prefab.newTokenizerFromTemplate(
+ config.tokenizerTemplate,
+ JX.copy({}, tconfig));
built.tokenizer.start();
return {
object: built.tokenizer,
- template: template
+ template: built.node
};
}
});
diff --git a/webroot/rsrc/js/application/policy/behavior-policy-rule-editor.js b/webroot/rsrc/js/application/policy/behavior-policy-rule-editor.js
index 3da2cdad5c..fd790644a1 100644
--- a/webroot/rsrc/js/application/policy/behavior-policy-rule-editor.js
+++ b/webroot/rsrc/js/application/policy/behavior-policy-rule-editor.js
@@ -1,175 +1,178 @@
/**
* @provides javelin-behavior-policy-rule-editor
* @requires javelin-behavior
* multirow-row-manager
* javelin-dom
* javelin-util
* phabricator-prefab
* javelin-json
*/
JX.behavior('policy-rule-editor', function(config) {
var root = JX.$(config.rootID);
var rows = [];
var data = {};
JX.DOM.listen(
root,
'click',
'create-rule',
function(e) {
e.kill();
new_rule(config.defaultRule);
});
JX.DOM.listen(
root,
'change',
'rule-select',
function(e) {
e.kill();
var row = e.getNode(JX.MultirowRowManager.getRowSigil());
var row_id = rules_manager.getRowID(row);
data[row_id].rule = data[row_id].ruleNode.value;
data[row_id].value = null;
redraw(row_id);
});
JX.DOM.listen(
JX.DOM.findAbove(root, 'form'),
['submit', 'didWorkflowSubmit'],
null,
function(e) {
var rules = JX.DOM.find(e.getNode('tag:form'), 'input', 'rules');
var value = [];
for (var ii = 0; ii < rows.length; ii++) {
var row_data = data[rows[ii]];
var row_dict = {
action: row_data.actionNode.value,
rule: row_data.rule,
value: row_data.getValue()
};
value.push(row_dict);
}
rules.value = JX.JSON.stringify(value);
});
var rules_table = JX.DOM.find(root, 'table', 'rules');
var rules_manager = new JX.MultirowRowManager(rules_table);
rules_manager.listen(
'row-removed',
function(row_id) {
delete data[row_id];
for (var ii = 0; ii < rows.length; ii++) {
if (rows[ii] == row_id) {
rows.splice(ii, 1);
break;
}
}
});
function new_rule(spec) {
var row = rules_manager.addRow([]);
var row_id = rules_manager.getRowID(row);
rows.push(row_id);
data[row_id] = JX.copy({}, spec);
redraw(row_id);
}
function redraw(row_id) {
var action_content = JX.Prefab.renderSelect(
config.actions,
data[row_id].action);
data[row_id].actionNode = action_content;
var action_cell = JX.$N('td', {className: 'action-cell'}, action_content);
var rule_content = JX.Prefab.renderSelect(
config.rules,
data[row_id].rule,
{sigil: 'rule-select'});
data[row_id].ruleNode = rule_content;
var rule_cell = JX.$N('td', {className: 'rule-cell'}, rule_content);
var input = render_input(data[row_id].rule, null);
var value_content = input.node;
data[row_id].getValue = input.get;
input.set(data[row_id].value);
var value_cell = JX.$N('td', {className: 'value-cell'}, value_content);
rules_manager.updateRow(row_id, [action_cell, rule_cell, value_cell]);
}
function render_input(rule, value) {
var node, get_fn, set_fn;
var type = config.types[rule];
var template = config.templates[rule];
switch (type) {
case 'tokenizer':
- node = JX.$H(template.markup).getNode();
- node.id = '';
-
var options = {
- root: node,
src: template.uri,
placeholder: template.placeholder,
+ browseURI: template.browseURI,
limit: template.limit
};
- var tokenizer = JX.Prefab.buildTokenizer(options).tokenizer;
+ var build = JX.Prefab.newTokenizerFromTemplate(
+ template.markup,
+ options);
+
+ node = build.node;
+
+ var tokenizer = build.tokenizer;
tokenizer.start();
get_fn = function() { return JX.keys(tokenizer.getTokens()); };
set_fn = function(map) {
if (!map) {
return;
}
for (var k in map) {
tokenizer.addToken(k, map[k]);
}
};
break;
case 'none':
node = null;
get_fn = JX.bag;
set_fn = JX.bag;
break;
case 'select':
node = JX.Prefab.renderSelect(
config.templates[rule].options,
value);
get_fn = function() { return node.value; };
set_fn = function(v) { node.value = v; };
break;
default:
case 'text':
node = JX.$N('input', {type: 'text'});
get_fn = function() { return node.value; };
set_fn = function(v) { node.value = v; };
break;
}
return {
node: node,
get: get_fn,
set: set_fn
};
}
for (var ii = 0; ii < config.data.length; ii++) {
new_rule(config.data[ii]);
}
});
diff --git a/webroot/rsrc/js/core/Prefab.js b/webroot/rsrc/js/core/Prefab.js
index 70e7e5946e..5856322fd3 100644
--- a/webroot/rsrc/js/core/Prefab.js
+++ b/webroot/rsrc/js/core/Prefab.js
@@ -1,291 +1,302 @@
/**
* @provides phabricator-prefab
* @requires javelin-install
* javelin-util
* javelin-dom
* javelin-typeahead
* javelin-tokenizer
* javelin-typeahead-preloaded-source
* javelin-typeahead-ondemand-source
* javelin-dom
* javelin-stratcom
* javelin-util
* @javelin
*/
/**
* Utilities for client-side rendering (the greatest thing in the world).
*/
JX.install('Prefab', {
statics : {
renderSelect : function(map, selected, attrs) {
var select = JX.$N('select', attrs || {});
for (var k in map) {
select.options[select.options.length] = new Option(map[k], k);
if (k == selected) {
select.value = k;
}
}
select.value = select.value || JX.keys(map)[0];
return select;
},
+ newTokenizerFromTemplate: function(markup, config) {
+ var template = JX.$H(markup).getFragment().firstChild;
+ var container = JX.DOM.find(template, 'div', 'tokenizer-container');
+
+ container.id = '';
+ config.root = container;
+
+ var build = JX.Prefab.buildTokenizer(config);
+ build.node = template;
+ return build;
+ },
/**
* Build a Phabricator tokenizer out of a configuration with application
* sorting, datasource and placeholder rules.
*
* - `id` Root tokenizer ID (alternatively, pass `root`).
* - `root` Root tokenizer node (replaces `id`).
* - `src` Datasource URI.
* - `ondemand` Optional, use an ondemand source.
* - `value` Optional, initial value.
* - `limit` Optional, token limit.
* - `placeholder` Optional, placeholder text.
* - `username` Optional, username to sort first (i.e., viewer).
* - `icons` Optional, map of icons.
*
*/
buildTokenizer : function(config) {
config.icons = config.icons || {};
var root;
try {
root = config.root || JX.$(config.id);
} catch (ex) {
// If the root element does not exist, just return without building
// anything. This happens in some cases -- like Conpherence -- where we
// may load a tokenizer but not put it in the document.
return;
}
var datasource;
// Default to an ondemand source if no alternate configuration is
// provided.
var ondemand = true;
if ('ondemand' in config) {
ondemand = config.ondemand;
}
if (ondemand) {
datasource = new JX.TypeaheadOnDemandSource(config.src);
} else {
datasource = new JX.TypeaheadPreloadedSource(config.src);
}
// Sort results so that the viewing user always comes up first; after
// that, prefer unixname matches to realname matches.
var sort_handler = function(value, list, cmp) {
var priority_hits = {};
var self_hits = {};
var tokens = this.tokenize(value);
for (var ii = 0; ii < list.length; ii++) {
var item = list[ii];
for (var jj = 0; jj < tokens.length; jj++) {
if (item.name.indexOf(tokens[jj]) === 0) {
priority_hits[item.id] = true;
}
}
if (!item.priority) {
continue;
}
if (config.username && item.priority == config.username) {
self_hits[item.id] = true;
}
for (var hh = 0; hh < tokens.length; hh++) {
if (item.priority.substr(0, tokens[hh].length) == tokens[hh]) {
priority_hits[item.id] = true;
}
}
}
list.sort(function(u, v) {
if (self_hits[u.id] != self_hits[v.id]) {
return self_hits[v.id] ? 1 : -1;
}
// If one result is open and one is closed, show the open result
// first. The "!" tricks here are becaused closed values are display
// strings, so the value is either `null` or some truthy string. If
// we compare the values directly, we'll apply this rule to two
// objects which are both closed but for different reasons, like
// "Archived" and "Disabled".
var u_open = !u.closed;
var v_open = !v.closed;
if (u_open != v_open) {
if (u_open) {
return -1;
} else {
return 1;
}
}
if (priority_hits[u.id] != priority_hits[v.id]) {
return priority_hits[v.id] ? 1 : -1;
}
// Sort users ahead of other result types.
if (u.priorityType != v.priorityType) {
if (u.priorityType == 'user') {
return -1;
}
if (v.priorityType == 'user') {
return 1;
}
}
return cmp(u, v);
});
};
datasource.setSortHandler(JX.bind(datasource, sort_handler));
datasource.setFilterHandler(JX.Prefab.filterClosedResults);
datasource.setTransformer(JX.Prefab.transformDatasourceResults);
var typeahead = new JX.Typeahead(
root,
JX.DOM.find(root, 'input', 'tokenizer-input'));
typeahead.setDatasource(datasource);
var tokenizer = new JX.Tokenizer(root);
tokenizer.setTypeahead(typeahead);
tokenizer.setRenderTokenCallback(function(value, key) {
var result = datasource.getResult(key);
var icon;
if (result) {
icon = result.icon;
value = result.displayName;
} else {
icon = config.icons[key];
}
if (icon) {
icon = JX.Prefab._renderIcon(icon);
}
// TODO: Maybe we should render these closed tags in grey? Figure out
// how we're going to use color.
return [icon, value];
});
if (config.placeholder) {
tokenizer.setPlaceholder(config.placeholder);
}
if (config.limit) {
tokenizer.setLimit(config.limit);
}
if (config.value) {
tokenizer.setInitialValue(config.value);
}
if (config.browseURI) {
tokenizer.setBrowseURI(config.browseURI);
}
JX.Stratcom.addData(root, {'tokenizer' : tokenizer});
return {
tokenizer: tokenizer
};
},
/**
* Filter callback for tokenizers and typeaheads which filters out closed
* or disabled objects unless they are the only options.
*/
filterClosedResults: function(value, list) {
// Look for any open result.
var has_open = false;
var ii;
for (ii = 0; ii < list.length; ii++) {
if (!list[ii].closed) {
has_open = true;
break;
}
}
if (!has_open) {
// Everything is closed, so just use it as-is.
return list;
}
// Otherwise, only display the open results.
var results = [];
for (ii = 0; ii < list.length; ii++) {
if (!list[ii].closed) {
results.push(list[ii]);
}
}
return results;
},
/**
* Transform results from a wire format into a usable format in a standard
* way.
*/
transformDatasourceResults: function(fields) {
var closed = fields[9];
var closed_ui;
if (closed) {
closed_ui = JX.$N(
'div',
{className: 'tokenizer-closed'},
closed);
}
var icon = fields[8];
var icon_ui;
if (icon) {
icon_ui = JX.Prefab._renderIcon(icon);
}
var display = JX.$N(
'div',
{className: 'tokenizer-result'},
[icon_ui, fields[4] || fields[0], closed_ui]);
if (closed) {
JX.DOM.alterClass(display, 'tokenizer-result-closed', true);
}
return {
name: fields[0],
displayName: fields[4] || fields[0],
display: display,
uri: fields[1],
id: fields[2],
priority: fields[3],
priorityType: fields[7],
imageURI: fields[6],
icon: icon,
closed: closed,
type: fields[5],
sprite: fields[10]
};
},
_renderIcon: function(icon) {
return JX.$N(
'span',
{className: 'phui-icon-view phui-font-fa ' + icon});
}
}
});

File Metadata

Mime Type
text/x-diff
Expires
Thu, Jul 3, 7:36 PM (4 h, 58 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
166275
Default Alt Text
(94 KB)

Event Timeline