mirror of
https://we.phorge.it/source/phorge.git
synced 2024-12-28 08:20:57 +01:00
Add remarkup support for Asana URIs
Summary: Ref T2852. Primarily, this expands API access to Asana. As a user-visible effect, it links Asana tasks in Remarkup. When a user enters an Asana URI, we register an onload behavior to make an Ajax call for the lookup. This respects privacy imposed by the API without creating a significant performance impact. Test Plan: {F47183} Reviewers: btrahan Reviewed By: btrahan CC: chad, aran Maniphest Tasks: T2852 Differential Revision: https://secure.phabricator.com/D6274
This commit is contained in:
parent
e723b7e119
commit
b22e52e40c
13 changed files with 389 additions and 10 deletions
|
@ -1652,6 +1652,20 @@ celerity_register_resource_map(array(
|
||||||
),
|
),
|
||||||
'disk' => '/rsrc/js/application/diffusion/behavior-pull-lastmodified.js',
|
'disk' => '/rsrc/js/application/diffusion/behavior-pull-lastmodified.js',
|
||||||
),
|
),
|
||||||
|
'javelin-behavior-doorkeeper-tag' =>
|
||||||
|
array(
|
||||||
|
'uri' => '/res/59480572/rsrc/js/application/doorkeeper/behavior-doorkeeper-tag.js',
|
||||||
|
'type' => 'js',
|
||||||
|
'requires' =>
|
||||||
|
array(
|
||||||
|
0 => 'javelin-behavior',
|
||||||
|
1 => 'javelin-dom',
|
||||||
|
2 => 'javelin-json',
|
||||||
|
3 => 'javelin-workflow',
|
||||||
|
4 => 'javelin-magical-init',
|
||||||
|
),
|
||||||
|
'disk' => '/rsrc/js/application/doorkeeper/behavior-doorkeeper-tag.js',
|
||||||
|
),
|
||||||
'javelin-behavior-error-log' =>
|
'javelin-behavior-error-log' =>
|
||||||
array(
|
array(
|
||||||
'uri' => '/res/acefdea7/rsrc/js/core/behavior-error-log.js',
|
'uri' => '/res/acefdea7/rsrc/js/core/behavior-error-log.js',
|
||||||
|
|
|
@ -537,7 +537,10 @@ phutil_register_library_map(array(
|
||||||
'DoorkeeperBridgeAsana' => 'applications/doorkeeper/bridge/DoorkeeperBridgeAsana.php',
|
'DoorkeeperBridgeAsana' => 'applications/doorkeeper/bridge/DoorkeeperBridgeAsana.php',
|
||||||
'DoorkeeperDAO' => 'applications/doorkeeper/storage/DoorkeeperDAO.php',
|
'DoorkeeperDAO' => 'applications/doorkeeper/storage/DoorkeeperDAO.php',
|
||||||
'DoorkeeperExternalObject' => 'applications/doorkeeper/storage/DoorkeeperExternalObject.php',
|
'DoorkeeperExternalObject' => 'applications/doorkeeper/storage/DoorkeeperExternalObject.php',
|
||||||
|
'DoorkeeperImportEngine' => 'applications/doorkeeper/engine/DoorkeeperImportEngine.php',
|
||||||
'DoorkeeperObjectRef' => 'applications/doorkeeper/engine/DoorkeeperObjectRef.php',
|
'DoorkeeperObjectRef' => 'applications/doorkeeper/engine/DoorkeeperObjectRef.php',
|
||||||
|
'DoorkeeperRemarkupRuleAsana' => 'applications/doorkeeper/remarkup/DoorkeeperRemarkupRuleAsana.php',
|
||||||
|
'DoorkeeperTagsController' => 'applications/doorkeeper/controller/DoorkeeperTagsController.php',
|
||||||
'DrydockAllocatorWorker' => 'applications/drydock/worker/DrydockAllocatorWorker.php',
|
'DrydockAllocatorWorker' => 'applications/drydock/worker/DrydockAllocatorWorker.php',
|
||||||
'DrydockApacheWebrootInterface' => 'applications/drydock/interface/webroot/DrydockApacheWebrootInterface.php',
|
'DrydockApacheWebrootInterface' => 'applications/drydock/interface/webroot/DrydockApacheWebrootInterface.php',
|
||||||
'DrydockBlueprint' => 'applications/drydock/blueprint/DrydockBlueprint.php',
|
'DrydockBlueprint' => 'applications/drydock/blueprint/DrydockBlueprint.php',
|
||||||
|
@ -745,6 +748,7 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorApplicationDifferential' => 'applications/differential/application/PhabricatorApplicationDifferential.php',
|
'PhabricatorApplicationDifferential' => 'applications/differential/application/PhabricatorApplicationDifferential.php',
|
||||||
'PhabricatorApplicationDiffusion' => 'applications/diffusion/application/PhabricatorApplicationDiffusion.php',
|
'PhabricatorApplicationDiffusion' => 'applications/diffusion/application/PhabricatorApplicationDiffusion.php',
|
||||||
'PhabricatorApplicationDiviner' => 'applications/diviner/application/PhabricatorApplicationDiviner.php',
|
'PhabricatorApplicationDiviner' => 'applications/diviner/application/PhabricatorApplicationDiviner.php',
|
||||||
|
'PhabricatorApplicationDoorkeeper' => 'applications/doorkeeper/application/PhabricatorApplicationDoorkeeper.php',
|
||||||
'PhabricatorApplicationDrydock' => 'applications/drydock/application/PhabricatorApplicationDrydock.php',
|
'PhabricatorApplicationDrydock' => 'applications/drydock/application/PhabricatorApplicationDrydock.php',
|
||||||
'PhabricatorApplicationFact' => 'applications/fact/application/PhabricatorApplicationFact.php',
|
'PhabricatorApplicationFact' => 'applications/fact/application/PhabricatorApplicationFact.php',
|
||||||
'PhabricatorApplicationFeed' => 'applications/feed/application/PhabricatorApplicationFeed.php',
|
'PhabricatorApplicationFeed' => 'applications/feed/application/PhabricatorApplicationFeed.php',
|
||||||
|
@ -2417,7 +2421,10 @@ phutil_register_library_map(array(
|
||||||
0 => 'DoorkeeperDAO',
|
0 => 'DoorkeeperDAO',
|
||||||
1 => 'PhabricatorPolicyInterface',
|
1 => 'PhabricatorPolicyInterface',
|
||||||
),
|
),
|
||||||
|
'DoorkeeperImportEngine' => 'Phobject',
|
||||||
'DoorkeeperObjectRef' => 'Phobject',
|
'DoorkeeperObjectRef' => 'Phobject',
|
||||||
|
'DoorkeeperRemarkupRuleAsana' => 'PhutilRemarkupRule',
|
||||||
|
'DoorkeeperTagsController' => 'PhabricatorController',
|
||||||
'DrydockAllocatorWorker' => 'PhabricatorWorker',
|
'DrydockAllocatorWorker' => 'PhabricatorWorker',
|
||||||
'DrydockApacheWebrootInterface' => 'DrydockWebrootInterface',
|
'DrydockApacheWebrootInterface' => 'DrydockWebrootInterface',
|
||||||
'DrydockCommandInterface' => 'DrydockInterface',
|
'DrydockCommandInterface' => 'DrydockInterface',
|
||||||
|
@ -2608,6 +2615,7 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorApplicationDifferential' => 'PhabricatorApplication',
|
'PhabricatorApplicationDifferential' => 'PhabricatorApplication',
|
||||||
'PhabricatorApplicationDiffusion' => 'PhabricatorApplication',
|
'PhabricatorApplicationDiffusion' => 'PhabricatorApplication',
|
||||||
'PhabricatorApplicationDiviner' => 'PhabricatorApplication',
|
'PhabricatorApplicationDiviner' => 'PhabricatorApplication',
|
||||||
|
'PhabricatorApplicationDoorkeeper' => 'PhabricatorApplication',
|
||||||
'PhabricatorApplicationDrydock' => 'PhabricatorApplication',
|
'PhabricatorApplicationDrydock' => 'PhabricatorApplication',
|
||||||
'PhabricatorApplicationFact' => 'PhabricatorApplication',
|
'PhabricatorApplicationFact' => 'PhabricatorApplication',
|
||||||
'PhabricatorApplicationFeed' => 'PhabricatorApplication',
|
'PhabricatorApplicationFeed' => 'PhabricatorApplication',
|
||||||
|
|
|
@ -15,12 +15,16 @@ final class DiffusionRemarkupRule
|
||||||
$min_unqualified = PhabricatorRepository::MINIMUM_UNQUALIFIED_HASH;
|
$min_unqualified = PhabricatorRepository::MINIMUM_UNQUALIFIED_HASH;
|
||||||
$min_qualified = PhabricatorRepository::MINIMUM_QUALIFIED_HASH;
|
$min_qualified = PhabricatorRepository::MINIMUM_QUALIFIED_HASH;
|
||||||
|
|
||||||
|
// NOTE: The "(?<!/)" negative lookbehind prevents this rule from matching
|
||||||
|
// hashes or hash-like substrings in most URLs. For example, this will not
|
||||||
|
// match: http://www.example.com/article/28903218328/
|
||||||
|
|
||||||
return
|
return
|
||||||
'r[A-Z]+[1-9]\d*'.
|
'r[A-Z]+[1-9]\d*'.
|
||||||
'|'.
|
'|'.
|
||||||
'r[A-Z]+[a-f0-9]{'.$min_qualified.',40}'.
|
'r[A-Z]+[a-f0-9]{'.$min_qualified.',40}'.
|
||||||
'|'.
|
'|'.
|
||||||
'[a-f0-9]{'.$min_unqualified.',40}';
|
'(?<!/)[a-f0-9]{'.$min_unqualified.',40}';
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function loadObjects(array $ids) {
|
protected function loadObjects(array $ids) {
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class PhabricatorApplicationDoorkeeper extends PhabricatorApplication {
|
||||||
|
|
||||||
|
public function canUninstall() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getBaseURI() {
|
||||||
|
return '/doorkeeper/';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function shouldAppearOnLaunchView() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRemarkupRules() {
|
||||||
|
return array(
|
||||||
|
new DoorkeeperRemarkupRuleAsana(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRoutes() {
|
||||||
|
return array(
|
||||||
|
'/doorkeeper/' => array(
|
||||||
|
'tags/' => 'DoorkeeperTagsController',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -25,6 +25,10 @@ final class DoorkeeperBridgeAsana extends DoorkeeperBridge {
|
||||||
->withAccountDomains(array($provider->getProviderDomain()))
|
->withAccountDomains(array($provider->getProviderDomain()))
|
||||||
->execute();
|
->execute();
|
||||||
|
|
||||||
|
if (!$accounts) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: If the user has several linked Asana accounts, we just pick the
|
// TODO: If the user has several linked Asana accounts, we just pick the
|
||||||
// first one arbitrarily. We might want to try using all of them or do
|
// first one arbitrarily. We might want to try using all of them or do
|
||||||
// something with more finesse. There's no UI way to link multiple accounts
|
// something with more finesse. There's no UI way to link multiple accounts
|
||||||
|
@ -47,10 +51,16 @@ final class DoorkeeperBridgeAsana extends DoorkeeperBridge {
|
||||||
|
|
||||||
$results = array();
|
$results = array();
|
||||||
foreach (Futures($futures) as $key => $future) {
|
foreach (Futures($futures) as $key => $future) {
|
||||||
$results[$key] = $future->resolve();
|
try {
|
||||||
|
$results[$key] = $future->resolve();
|
||||||
|
} catch (Exception $ex) {
|
||||||
|
// TODO: For now, ignore this stuff.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($refs as $ref) {
|
foreach ($refs as $ref) {
|
||||||
|
$ref->setAttribute('name', pht('Asana Task %s', $ref->getObjectID()));
|
||||||
|
|
||||||
$result = idx($results, $ref->getObjectKey());
|
$result = idx($results, $ref->getObjectKey());
|
||||||
if (!$result) {
|
if (!$result) {
|
||||||
continue;
|
continue;
|
||||||
|
@ -58,7 +68,8 @@ final class DoorkeeperBridgeAsana extends DoorkeeperBridge {
|
||||||
|
|
||||||
$ref->setIsVisible(true);
|
$ref->setIsVisible(true);
|
||||||
$ref->setAttribute('asana.data', $result);
|
$ref->setAttribute('asana.data', $result);
|
||||||
$ref->setAttribute('name', $result['name']);
|
$ref->setAttribute('fullname', pht('Asana: %s', $result['name']));
|
||||||
|
$ref->setAttribute('title', $result['name']);
|
||||||
$ref->setAttribute('description', $result['notes']);
|
$ref->setAttribute('description', $result['notes']);
|
||||||
|
|
||||||
$obj = $ref->getExternalObject();
|
$obj = $ref->getExternalObject();
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class DoorkeeperTagsController extends PhabricatorController {
|
||||||
|
|
||||||
|
public function processRequest() {
|
||||||
|
$request = $this->getRequest();
|
||||||
|
$viewer = $request->getUser();
|
||||||
|
|
||||||
|
$tags = $request->getStr('tags');
|
||||||
|
$tags = json_decode($tags, true);
|
||||||
|
if (!is_array($tags)) {
|
||||||
|
$tags = array();
|
||||||
|
}
|
||||||
|
|
||||||
|
$refs = array();
|
||||||
|
$id_map = array();
|
||||||
|
foreach ($tags as $tag_spec) {
|
||||||
|
$tag = $tag_spec['ref'];
|
||||||
|
$ref = id(new DoorkeeperObjectRef())
|
||||||
|
->setApplicationType($tag[0])
|
||||||
|
->setApplicationDomain($tag[1])
|
||||||
|
->setObjectType($tag[2])
|
||||||
|
->setObjectID($tag[3]);
|
||||||
|
|
||||||
|
$key = $ref->getObjectKey();
|
||||||
|
$id_map[$key] = $tag_spec['id'];
|
||||||
|
$refs[$key] = $ref;
|
||||||
|
}
|
||||||
|
|
||||||
|
$refs = id(new DoorkeeperImportEngine())
|
||||||
|
->setViewer($viewer)
|
||||||
|
->setRefs($refs)
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
$results = array();
|
||||||
|
foreach ($refs as $key => $ref) {
|
||||||
|
if (!$ref->getIsVisible()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$uri = $ref->getExternalObject()->getObjectURI();
|
||||||
|
if (!$uri) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = $id_map[$key];
|
||||||
|
|
||||||
|
$tag = id(new PhabricatorTagView())
|
||||||
|
->setID($id)
|
||||||
|
->setName($ref->getFullName())
|
||||||
|
->setHref($uri)
|
||||||
|
->setType(PhabricatorTagView::TYPE_OBJECT)
|
||||||
|
->setExternal(true)
|
||||||
|
->render();
|
||||||
|
|
||||||
|
$results[] = array(
|
||||||
|
'id' => $id,
|
||||||
|
'markup' => $tag,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return id(new AphrontAjaxResponse())->setContent(
|
||||||
|
array(
|
||||||
|
'tags' => $results,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class DoorkeeperImportEngine extends Phobject {
|
||||||
|
|
||||||
|
private $viewer;
|
||||||
|
private $refs;
|
||||||
|
|
||||||
|
public function setViewer(PhabricatorUser $viewer) {
|
||||||
|
$this->viewer = $viewer;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getViewer() {
|
||||||
|
return $this->viewer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setRefs(array $refs) {
|
||||||
|
assert_instances_of($refs, 'DoorkeeperObjectRef');
|
||||||
|
$this->refs = $refs;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRefs() {
|
||||||
|
return $this->refs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function execute() {
|
||||||
|
$refs = $this->getRefs();
|
||||||
|
$viewer = $this->getViewer();
|
||||||
|
|
||||||
|
$keys = mpull($refs, 'getObjectKey');
|
||||||
|
if ($keys) {
|
||||||
|
$xobjs = id(new DoorkeeperExternalObject())->loadAllWhere(
|
||||||
|
'objectKey IN (%Ls)',
|
||||||
|
$keys);
|
||||||
|
$xobjs = mpull($xobjs, null, 'getObjectKey');
|
||||||
|
foreach ($refs as $ref) {
|
||||||
|
$xobj = idx($xobjs, $ref->getObjectKey());
|
||||||
|
if (!$xobj) {
|
||||||
|
$xobj = $ref->newExternalObject()
|
||||||
|
->setImporterPHID($viewer->getPHID());
|
||||||
|
}
|
||||||
|
$ref->attachExternalObject($xobj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$bridges = id(new PhutilSymbolLoader())
|
||||||
|
->setAncestorClass('DoorkeeperBridge')
|
||||||
|
->loadObjects();
|
||||||
|
|
||||||
|
foreach ($bridges as $key => $bridge) {
|
||||||
|
if (!$bridge->isEnabled()) {
|
||||||
|
unset($bridges[$key]);
|
||||||
|
}
|
||||||
|
$bridge->setViewer($viewer);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($bridges as $bridge) {
|
||||||
|
$bridge_refs = array();
|
||||||
|
foreach ($refs as $key => $ref) {
|
||||||
|
if ($bridge->canPullRef($ref)) {
|
||||||
|
$bridge_refs[$key] = $ref;
|
||||||
|
unset($refs[$key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($bridge_refs) {
|
||||||
|
$bridge->pullRefs($bridge_refs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->getRefs();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -44,7 +44,7 @@ final class DoorkeeperObjectRef extends Phobject {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getAttribute($key, $default = null) {
|
public function getAttribute($key, $default = null) {
|
||||||
return idx($this->attribute, $key, $default);
|
return idx($this->attributes, $key, $default);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setAttribute($key, $value) {
|
public function setAttribute($key, $value) {
|
||||||
|
@ -91,6 +91,13 @@ final class DoorkeeperObjectRef extends Phobject {
|
||||||
return $this->applicationType;
|
return $this->applicationType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getFullName() {
|
||||||
|
return coalesce(
|
||||||
|
$this->getAttribute('fullname'),
|
||||||
|
$this->getAttribute('name'),
|
||||||
|
pht('External Object'));
|
||||||
|
}
|
||||||
|
|
||||||
public function getObjectKey() {
|
public function getObjectKey() {
|
||||||
if (!$this->objectKey) {
|
if (!$this->objectKey) {
|
||||||
$this->objectKey = PhabricatorHash::digestForIndex(
|
$this->objectKey = PhabricatorHash::digestForIndex(
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class DoorkeeperRemarkupRuleAsana
|
||||||
|
extends PhutilRemarkupRule {
|
||||||
|
|
||||||
|
const KEY_TAGS = 'doorkeeper.tags';
|
||||||
|
|
||||||
|
public function apply($text) {
|
||||||
|
return preg_replace_callback(
|
||||||
|
'@https://app\\.asana\\.com/0/(\\d+)/(\\d+)@',
|
||||||
|
array($this, 'markupAsanaLink'),
|
||||||
|
$text);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function markupAsanaLink($matches) {
|
||||||
|
$key = self::KEY_TAGS;
|
||||||
|
$engine = $this->getEngine();
|
||||||
|
$token = $engine->storeText('AsanaDoorkeeper');
|
||||||
|
|
||||||
|
$tags = $engine->getTextMetadata($key, array());
|
||||||
|
|
||||||
|
$tags[] = array(
|
||||||
|
'token' => $token,
|
||||||
|
'href' => $matches[0],
|
||||||
|
'tag' => array(
|
||||||
|
'ref' => array('asana', 'asana.com', 'asana:task', $matches[2]),
|
||||||
|
'extra' => array(
|
||||||
|
'asana.context' => $matches[1],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
$engine->setTextMetadata($key, $tags);
|
||||||
|
|
||||||
|
return $token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function didMarkupText() {
|
||||||
|
$key = self::KEY_TAGS;
|
||||||
|
$engine = $this->getEngine();
|
||||||
|
$tags = $engine->getTextMetadata($key, array());
|
||||||
|
|
||||||
|
if (!$tags) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$refs = array();
|
||||||
|
foreach ($tags as $spec) {
|
||||||
|
$tag_id = celerity_generate_unique_node_id();
|
||||||
|
|
||||||
|
$refs[] = array(
|
||||||
|
'id' => $tag_id,
|
||||||
|
) + $spec['tag'];
|
||||||
|
|
||||||
|
$view = id(new PhabricatorTagView())
|
||||||
|
->setID($tag_id)
|
||||||
|
->setName($spec['href'])
|
||||||
|
->setHref($spec['href'])
|
||||||
|
->setType(PhabricatorTagView::TYPE_OBJECT)
|
||||||
|
->setExternal(true);
|
||||||
|
|
||||||
|
$engine->overwriteStoredText($spec['token'], $view);
|
||||||
|
}
|
||||||
|
|
||||||
|
Javelin::initBehavior('doorkeeper-tag', array('tags' => $refs));
|
||||||
|
|
||||||
|
$engine->setTextMetadata($key, array());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -213,6 +213,8 @@ final class PhabricatorObjectHandleData {
|
||||||
return mpull($xusrs, null, 'getPHID');
|
return mpull($xusrs, null, 'getPHID');
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return array();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function loadHandles() {
|
public function loadHandles() {
|
||||||
|
|
|
@ -449,12 +449,6 @@ final class PhabricatorMarkupEngine {
|
||||||
$rules[] = new PhabricatorRemarkupRuleYoutube();
|
$rules[] = new PhabricatorRemarkupRuleYoutube();
|
||||||
}
|
}
|
||||||
|
|
||||||
$rules[] = new PhutilRemarkupRuleHyperlink();
|
|
||||||
$rules[] = new PhrictionRemarkupRule();
|
|
||||||
|
|
||||||
$rules[] = new PhabricatorRemarkupRuleEmbedFile();
|
|
||||||
$rules[] = new PhabricatorCountdownRemarkupRule();
|
|
||||||
|
|
||||||
$applications = PhabricatorApplication::getAllInstalledApplications();
|
$applications = PhabricatorApplication::getAllInstalledApplications();
|
||||||
foreach ($applications as $application) {
|
foreach ($applications as $application) {
|
||||||
foreach ($application->getRemarkupRules() as $rule) {
|
foreach ($application->getRemarkupRules() as $rule) {
|
||||||
|
@ -462,6 +456,12 @@ final class PhabricatorMarkupEngine {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$rules[] = new PhutilRemarkupRuleHyperlink();
|
||||||
|
$rules[] = new PhrictionRemarkupRule();
|
||||||
|
|
||||||
|
$rules[] = new PhabricatorRemarkupRuleEmbedFile();
|
||||||
|
$rules[] = new PhabricatorCountdownRemarkupRule();
|
||||||
|
|
||||||
if ($options['macros']) {
|
if ($options['macros']) {
|
||||||
$rules[] = new PhabricatorRemarkupRuleImageMacro();
|
$rules[] = new PhabricatorRemarkupRuleImageMacro();
|
||||||
$rules[] = new PhabricatorRemarkupRuleMeme();
|
$rules[] = new PhabricatorRemarkupRuleMeme();
|
||||||
|
|
|
@ -28,6 +28,17 @@ final class PhabricatorTagView extends AphrontView {
|
||||||
private $dotColor;
|
private $dotColor;
|
||||||
private $barColor;
|
private $barColor;
|
||||||
private $closed;
|
private $closed;
|
||||||
|
private $external;
|
||||||
|
private $id;
|
||||||
|
|
||||||
|
public function setID($id) {
|
||||||
|
$this->id = $id;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getID() {
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
public function setType($type) {
|
public function setType($type) {
|
||||||
$this->type = $type;
|
$this->type = $type;
|
||||||
|
@ -135,20 +146,24 @@ final class PhabricatorTagView extends AphrontView {
|
||||||
return javelin_tag(
|
return javelin_tag(
|
||||||
'a',
|
'a',
|
||||||
array(
|
array(
|
||||||
|
'id' => $this->id,
|
||||||
'href' => $this->href,
|
'href' => $this->href,
|
||||||
'class' => implode(' ', $classes),
|
'class' => implode(' ', $classes),
|
||||||
'sigil' => 'hovercard',
|
'sigil' => 'hovercard',
|
||||||
'meta' => array(
|
'meta' => array(
|
||||||
'hoverPHID' => $this->phid,
|
'hoverPHID' => $this->phid,
|
||||||
),
|
),
|
||||||
|
'target' => $this->external ? '_blank' : null,
|
||||||
),
|
),
|
||||||
array($bar, $content));
|
array($bar, $content));
|
||||||
} else {
|
} else {
|
||||||
return phutil_tag(
|
return phutil_tag(
|
||||||
$this->href ? 'a' : 'span',
|
$this->href ? 'a' : 'span',
|
||||||
array(
|
array(
|
||||||
|
'id' => $this->id,
|
||||||
'href' => $this->href,
|
'href' => $this->href,
|
||||||
'class' => implode(' ', $classes),
|
'class' => implode(' ', $classes),
|
||||||
|
'target' => $this->external ? '_blank' : null,
|
||||||
),
|
),
|
||||||
array($bar, $content));
|
array($bar, $content));
|
||||||
}
|
}
|
||||||
|
@ -180,4 +195,13 @@ final class PhabricatorTagView extends AphrontView {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function setExternal($external) {
|
||||||
|
$this->external = $external;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getExternal() {
|
||||||
|
return $this->external;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
/**
|
||||||
|
* @provides javelin-behavior-doorkeeper-tag
|
||||||
|
* @requires javelin-behavior
|
||||||
|
* javelin-dom
|
||||||
|
* javelin-json
|
||||||
|
* javelin-workflow
|
||||||
|
* javelin-magical-init
|
||||||
|
*/
|
||||||
|
|
||||||
|
JX.behavior('doorkeeper-tag', function(config, statics) {
|
||||||
|
statics.tags = (statics.tags || []).concat(config.tags);
|
||||||
|
statics.cache = statics.cache || {};
|
||||||
|
|
||||||
|
// NOTE: We keep a cache in the browser of external objects that we've already
|
||||||
|
// looked up. This is mostly to keep previews from being flickery messes.
|
||||||
|
|
||||||
|
var load = function() {
|
||||||
|
var tags = statics.tags;
|
||||||
|
statics.tags = [];
|
||||||
|
|
||||||
|
if (!tags.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var have = [];
|
||||||
|
var need = [];
|
||||||
|
var keys = {};
|
||||||
|
|
||||||
|
var draw = function(tags) {
|
||||||
|
for (var ii = 0; ii < tags.length; ii++) {
|
||||||
|
try {
|
||||||
|
JX.DOM.replace(JX.$(tags[ii].id), JX.$H(tags[ii].markup));
|
||||||
|
} catch (ignored) {
|
||||||
|
// The tag may have been wiped out of the body by the time the
|
||||||
|
// response returns, for whatever reason. That's fine, just don't
|
||||||
|
// bother drawing it.
|
||||||
|
}
|
||||||
|
statics.cache[keys[tags[ii].id]] = tags[ii].markup;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (var ii = 0; ii < tags.length; ii++) {
|
||||||
|
var tag_key = tags[ii].ref.join('@');
|
||||||
|
if (tag_key in statics.cache) {
|
||||||
|
have.push({id: tags[ii].id, markup: statics.cache[tag_key]});
|
||||||
|
} else {
|
||||||
|
need.push(tags[ii]);
|
||||||
|
keys[tags[ii].id] = tag_key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (have.length) {
|
||||||
|
draw(have);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (need.length) {
|
||||||
|
new JX.Workflow('/doorkeeper/tags/', {tags: JX.JSON.stringify(need)})
|
||||||
|
.setHandler(function(r) { draw(r.tags); })
|
||||||
|
.start();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
JX.onload(load);
|
||||||
|
});
|
||||||
|
|
Loading…
Reference in a new issue