1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-22 14:52:41 +01:00

Merge branch 'master' into phutil_tag

(Sync.)
This commit is contained in:
epriestley 2013-02-04 06:19:16 -08:00
commit 0f1bdbe147
59 changed files with 876 additions and 346 deletions

3
.gitignore vendored
View file

@ -24,3 +24,6 @@
/conf/local/local.json /conf/local/local.json
/conf/local/ENVIRONMENT /conf/local/ENVIRONMENT
/conf/local/VERSION /conf/local/VERSION
# Impact Font
/resources/font/impact.ttf

View file

@ -798,13 +798,11 @@ return array(
// behalf, silencing the warning. // behalf, silencing the warning.
'phabricator.timezone' => null, 'phabricator.timezone' => null,
// When unhandled exceptions occur, stack traces are hidden by default. // Show stack traces when unhandled exceptions occur, force reloading of
// You can enable traces for development to make it easier to debug problems. // static resources (skipping the cache), show an error callout if a page
'phabricator.show-stack-traces' => false, // generated PHP errors, warnings, or notices, force disk reads when
// reloading. This option should not be enabled in production.
// Shows an error callout if a page generated PHP errors, warnings or notices. 'phabricator.developer-mode' => false,
// This makes it harder to miss problems while developing Phabricator.
'phabricator.show-error-callout' => false,
// When users write comments which have URIs, they'll be automatically linked // When users write comments which have URIs, they'll be automatically linked
// if the protocol appears in this set. This whitelist is primarily to prevent // if the protocol appears in this set. This whitelist is primarily to prevent
@ -1220,15 +1218,6 @@ return array(
// unlikely that you need to modify this. // unlikely that you need to modify this.
'celerity.resource-hash' => 'd9455ea150622ee044f7931dabfa52aa', 'celerity.resource-hash' => 'd9455ea150622ee044f7931dabfa52aa',
// In a development environment, it is desirable to force static resources
// (CSS and JS) to be read from disk on every request, so that edits to them
// appear when you reload the page even if you haven't updated the resource
// maps. This setting ensures requests will be verified against the state on
// disk. Generally, you should leave this off in production (caching behavior
// and performance improve with it off) but turn it on in development. (These
// settings are the defaults.)
'celerity.force-disk-reads' => false,
// Minify static resources by removing whitespace and comments. You should // Minify static resources by removing whitespace and comments. You should
// enable this in production, but disable it in development. // enable this in production, but disable it in development.
'celerity.minify' => false, 'celerity.minify' => false,

View file

@ -2,10 +2,7 @@
return array( return array(
'phabricator.developer-mode' => true,
'darkconsole.enabled' => true, 'darkconsole.enabled' => true,
'celerity.force-disk-reads' => true,
'phabricator.show-stack-traces' => true,
'phabricator.show-error-callout' => true,
'celerity.minify' => false,
) + phabricator_read_config_file('default'); ) + phabricator_read_config_file('default');

View file

@ -133,17 +133,18 @@ JX.install('Event', {
var r = this.getRawEvent(); var r = this.getRawEvent();
return r.which == 3 || r.button == 2; return r.which == 3 || r.button == 2;
}, },
/** /**
* Determine if a click event is a normal click (left mouse button, no * Determine if a mouse event is a normal event (left mouse button, no
* modifier keys). * modifier keys).
* *
* @return bool * @return bool
* @task info * @task info
*/ */
isNormalClick : function() { isNormalMouseEvent : function() {
if (this.getType() != 'click') { var supportedEvents = ['click','mouseup','mousedown'];
if (supportedEvents.indexOf(this.getType()) == -1) {
return false; return false;
} }
@ -164,6 +165,22 @@ JX.install('Event', {
}, },
/**
* Determine if a click event is a normal click (left mouse button, no
* modifier keys).
*
* @return bool
* @task info
*/
isNormalClick : function() {
if (this.getType() != 'click') {
return false;
}
return this.isNormalMouseEvent();
},
/** /**
* Get the node corresponding to the specified key in this event's node map. * Get the node corresponding to the specified key in this event's node map.
* This is a simple helper method that makes the API for accessing nodes * This is a simple helper method that makes the API for accessing nodes

View file

@ -978,6 +978,7 @@ phutil_register_library_map(array(
'PhabricatorMarkupInterface' => 'infrastructure/markup/PhabricatorMarkupInterface.php', 'PhabricatorMarkupInterface' => 'infrastructure/markup/PhabricatorMarkupInterface.php',
'PhabricatorMenuItemView' => 'view/layout/PhabricatorMenuItemView.php', 'PhabricatorMenuItemView' => 'view/layout/PhabricatorMenuItemView.php',
'PhabricatorMenuView' => 'view/layout/PhabricatorMenuView.php', 'PhabricatorMenuView' => 'view/layout/PhabricatorMenuView.php',
'PhabricatorMenuViewTestCase' => 'view/layout/__tests__/PhabricatorMenuViewTestCase.php',
'PhabricatorMercurialGraphStream' => 'applications/repository/daemon/PhabricatorMercurialGraphStream.php', 'PhabricatorMercurialGraphStream' => 'applications/repository/daemon/PhabricatorMercurialGraphStream.php',
'PhabricatorMetaMTAAttachment' => 'applications/metamta/storage/PhabricatorMetaMTAAttachment.php', 'PhabricatorMetaMTAAttachment' => 'applications/metamta/storage/PhabricatorMetaMTAAttachment.php',
'PhabricatorMetaMTAConfigOptions' => 'applications/config/option/PhabricatorMetaMTAConfigOptions.php', 'PhabricatorMetaMTAConfigOptions' => 'applications/config/option/PhabricatorMetaMTAConfigOptions.php',
@ -2395,6 +2396,7 @@ phutil_register_library_map(array(
'PhabricatorMarkupCache' => 'PhabricatorCacheDAO', 'PhabricatorMarkupCache' => 'PhabricatorCacheDAO',
'PhabricatorMenuItemView' => 'AphrontTagView', 'PhabricatorMenuItemView' => 'AphrontTagView',
'PhabricatorMenuView' => 'AphrontTagView', 'PhabricatorMenuView' => 'AphrontTagView',
'PhabricatorMenuViewTestCase' => 'PhabricatorTestCase',
'PhabricatorMetaMTAConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorMetaMTAConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorMetaMTAController' => 'PhabricatorController', 'PhabricatorMetaMTAController' => 'PhabricatorController',
'PhabricatorMetaMTADAO' => 'PhabricatorLiskDAO', 'PhabricatorMetaMTADAO' => 'PhabricatorLiskDAO',

View file

@ -228,6 +228,26 @@ final class AphrontRequest {
$more_info = "(This was a web request, {$token_info}.)"; $more_info = "(This was a web request, {$token_info}.)";
} }
// Give a more detailed explanation of how to avoid the exception
// in developer mode.
if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) {
$more_info = $more_info .
"To avoid this error, use phabricator_form() to construct forms. " .
"If you are already using phabricator_form(), make sure the form " .
"'action' uses a relative URI (i.e., begins with a '/'). Forms " .
"using absolute URIs do not include CSRF tokens, to prevent " .
"leaking tokens to external sites.\n\n" .
"If this page performs writes which do not require CSRF " .
"protection (usually, filling caches or logging), you can use " .
"AphrontWriteGuard::beginScopedUnguardedWrites() to temporarily " .
"bypass CSRF protection while writing. You should use this only " .
"for writes which can not be protected with normal CSRF " .
"mechanisms.\n\n" .
"Some UI elements (like PhabricatorActionListView) also have " .
"methods which will allow you to render links as forms (like " .
"setRenderAsForm(true)).";
}
// This should only be able to happen if you load a form, pull your // This should only be able to happen if you load a form, pull your
// internet for 6 hours, and then reconnect and immediately submit, // internet for 6 hours, and then reconnect and immediately submit,
// but give the user some indication of what happened since the workflow // but give the user some indication of what happened since the workflow

View file

@ -238,7 +238,7 @@ class AphrontDefaultApplicationConfiguration
"schema is up to date."; "schema is up to date.";
} }
if (PhabricatorEnv::getEnvConfig('phabricator.show-stack-traces')) { if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) {
$trace = $this->renderStackTrace($ex->getTrace(), $user); $trace = $this->renderStackTrace($ex->getTrace(), $user);
} else { } else {
$trace = null; $trace = null;

View file

@ -22,7 +22,6 @@ final class PhabricatorApplicationAuth extends PhabricatorApplication {
$item->setIcon('power'); $item->setIcon('power');
$item->setWorkflow(true); $item->setWorkflow(true);
$item->setHref('/logout/'); $item->setHref('/logout/');
$item->setSortOrder(2.0);
$item->setSelected(($controller instanceof PhabricatorLogoutController)); $item->setSelected(($controller instanceof PhabricatorLogoutController));
$items[] = $item; $items[] = $item;
} }

View file

@ -70,9 +70,10 @@ final class PhabricatorOAuthFailureView extends AphrontView {
$provider_key = $provider->getProviderKey(); $provider_key = $provider->getProviderKey();
$diagnose = hsprintf( $diagnose = hsprintf(
'<a href="/oauth/'.$provider_key.'/diagnose/" class="button green">'. '<a href="/oauth/%s/diagnose/" class="button green">'.
'Diagnose %s OAuth Problems'. 'Diagnose %s OAuth Problems'.
'</a>', '</a>',
$provider_key,
$provider_name); $provider_name);
} }

View file

@ -66,7 +66,9 @@ abstract class PhabricatorApplication {
$uninstalled = $uninstalled =
PhabricatorEnv::getEnvConfig('phabricator.uninstalled-applications'); PhabricatorEnv::getEnvConfig('phabricator.uninstalled-applications');
if (isset($uninstalled[get_class($this)])) { if (!$this->canUninstall()) {
return true;
} else if (isset($uninstalled[get_class($this)])) {
return false; return false;
} else { } else {
return true; return true;
@ -227,8 +229,21 @@ abstract class PhabricatorApplication {
/* -( Application Management )--------------------------------------------- */ /* -( Application Management )--------------------------------------------- */
public static function getAllApplications() { public static function getByClass($class_name) {
$selected = null;
$applications = PhabricatorApplication::getAllApplications();
foreach ($applications as $application) {
if (get_class($application) == $class_name) {
$selected = $application;
break;
}
}
return $selected;
}
public static function getAllApplications() {
$classes = id(new PhutilSymbolLoader()) $classes = id(new PhutilSymbolLoader())
->setAncestorClass(__CLASS__) ->setAncestorClass(__CLASS__)
->setConcreteOnly(true) ->setConcreteOnly(true)
@ -241,6 +256,13 @@ abstract class PhabricatorApplication {
$apps[] = $app; $apps[] = $app;
} }
// Reorder the applications into "application order". Notably, this ensures
// their event handlers register in application order.
$apps = msort($apps, 'getApplicationOrder');
$apps = mgroup($apps, 'getApplicationGroup');
$apps = array_select_keys($apps, self::getApplicationGroups()) + $apps;
$apps = array_mergev($apps);
return $apps; return $apps;
} }
@ -254,31 +276,24 @@ abstract class PhabricatorApplication {
PhabricatorEnv::getEnvConfig('phabricator.uninstalled-applications'); PhabricatorEnv::getEnvConfig('phabricator.uninstalled-applications');
if (empty($applications)) { if (empty($applications)) {
$classes = id(new PhutilSymbolLoader()) $all_applications = self::getAllApplications();
->setAncestorClass(__CLASS__)
->setConcreteOnly(true)
->selectAndLoadSymbols();
$apps = array(); $apps = array();
foreach ($classes as $class) { foreach ($all_applications as $app) {
$class = get_class($app);
if (isset($uninstalled[$class['name']])) { if (isset($uninstalled[$class])) {
continue;
}
$app = newv($class['name'], array());
if (!$app->isEnabled()) {
continue; continue;
} }
if (!$show_beta && $app->isBeta()) { if (!$app->isEnabled()) {
continue; continue;
} }
$apps[] = $app; if (!$show_beta && $app->isBeta()) {
continue;
}
$apps[] = $app;
} }
$applications = $apps; $applications = $apps;

View file

@ -86,48 +86,18 @@ final class PhabricatorDeveloperConfigOptions
"data to look at eventually). In development, it may be useful to ". "data to look at eventually). In development, it may be useful to ".
"set it to 1 in order to debug performance problems.\n\n". "set it to 1 in order to debug performance problems.\n\n".
"NOTE: You must install XHProf for profiling to work.")), "NOTE: You must install XHProf for profiling to work.")),
$this->newOption('phabricator.show-stack-traces', 'bool', false) $this->newOption('phabricator.developer-mode', 'bool', false)
->setBoolOptions( ->setBoolOptions(
array( array(
pht('Show stack traces'), pht('Enable developer mode'),
pht('Hide stack traces'), pht('Disable developer mode'),
)) ))
->setSummary(pht("Show stack traces when unhandled exceptions occur.")) ->setSummary(pht("Enable verbose error reporting and disk reads."))
->setDescription( ->setDescription(
pht( pht(
"When unhandled exceptions occur, stack traces are hidden by ". "This option enables verbose error reporting (stack traces, ".
"default. You can enable traces for development to make it easier ". "error callouts) and forces disk reads of static assets on ".
"to debug problems.")), "every reload.")),
$this->newOption('phabricator.show-error-callout', 'bool', false)
->setBoolOptions(
array(
pht('Show error callout'),
pht('Hide error callout'),
))
->setSummary(pht("Show error callout."))
->setDescription(
pht(
"Shows an error callout if a page generated PHP errors, warnings ".
"or notices. This makes it harder to miss problems while ".
"developing Phabricator. A callout is simply a red error at the ".
"top of the page.")),
$this->newOption('celerity.force-disk-reads', 'bool', false)
->setBoolOptions(
array(
pht('Force disk reads'),
pht("Don't force disk reads"),
))
->setSummary(pht("Force Celerity to read from disk on every request."))
->setDescription(
pht(
"In a development environment, it is desirable to force static ".
"resources (CSS and JS) to be read from disk on every request, so ".
"that edits to them appear when you reload the page even if you ".
"haven't updated the resource maps. This setting ensures requests ".
"will be verified against the state on disk. Generally, you ".
"should leave this off in production (caching behavior and ".
"performance improve with it off) but turn it on in development. ".
"(These settings are the defaults.)")),
$this->newOption('celerity.minify', 'bool', false) $this->newOption('celerity.minify', 'bool', false)
->setBoolOptions( ->setBoolOptions(
array( array(

View file

@ -10,6 +10,15 @@ final class PhabricatorConfigResponse extends AphrontHTMLResponse {
} }
public function buildResponseString() { public function buildResponseString() {
// Check to make sure we aren't requesting this via ajax or conduit
if (isset($_REQUEST['__ajax__']) || isset($_REQUEST['__conduit__'])) {
// We don't want to flood the console with html, just return a simple
// message for now.
return pht(
"This install has a fatal setup error, access the internet web ".
"version to view details and resolve it.");
}
$resources = $this->buildResources(); $resources = $this->buildResources();
$view = $this->view->render(); $view = $this->view->render();

View file

@ -44,6 +44,7 @@ final class ConpherenceUpdateController extends
'ip' => $request->getRemoteAddr() 'ip' => $request->getRemoteAddr()
)); ));
$editor = id(new ConpherenceEditor()) $editor = id(new ConpherenceEditor())
->setContinueOnNoEffect($request->isContinueRequest())
->setContentSource($content_source) ->setContentSource($content_source)
->setActor($user); ->setActor($user);
@ -55,28 +56,6 @@ final class ConpherenceUpdateController extends
$conpherence, $conpherence,
$message $message
); );
$time = time();
$conpherence->openTransaction();
$xactions = $editor->applyTransactions($conpherence, $xactions);
$last_xaction = end($xactions);
$xaction_phid = $last_xaction->getPHID();
$behind = ConpherenceParticipationStatus::BEHIND;
$up_to_date = ConpherenceParticipationStatus::UP_TO_DATE;
$participants = $conpherence->getParticipants();
foreach ($participants as $phid => $participant) {
if ($phid != $user->getPHID()) {
if ($participant->getParticipationStatus() != $behind) {
$participant->setBehindTransactionPHID($xaction_phid);
}
$participant->setParticipationStatus($behind);
$participant->setDateTouched($time);
} else {
$participant->setParticipationStatus($up_to_date);
$participant->setDateTouched($time);
}
$participant->save();
}
$updated = $conpherence->saveTransaction();
break; break;
case 'metadata': case 'metadata':
$xactions = array(); $xactions = array();
@ -112,23 +91,25 @@ final class ConpherenceUpdateController extends
->setTransactionType(ConpherenceTransactionType::TYPE_TITLE) ->setTransactionType(ConpherenceTransactionType::TYPE_TITLE)
->setNewValue($title); ->setNewValue($title);
} }
if ($xactions) {
$conpherence->openTransaction();
$xactions = $editor
->setContinueOnNoEffect(true)
->applyTransactions($conpherence, $xactions);
$updated = $conpherence->saveTransaction();
} else if (empty($errors)) {
$errors[] = pht(
'That was a non-update. Try cancel.'
);
}
break; break;
default: default:
throw new Exception('Unknown action: '.$action); throw new Exception('Unknown action: '.$action);
break; break;
} }
if ($xactions) {
try {
$xactions = $editor->applyTransactions($conpherence, $xactions);
$updated = true;
} catch (PhabricatorApplicationTransactionNoEffectException $ex) {
return id(new PhabricatorApplicationTransactionNoEffectResponse())
->setCancelURI($this->getApplicationURI($conpherence_id.'/'))
->setException($ex);
}
} else if (empty($errors)) {
$errors[] = pht(
'That was a non-update. Try cancel.'
);
}
} }
if ($updated) { if ($updated) {

View file

@ -128,6 +128,7 @@ final class ConpherenceViewController extends
$form = $form =
id(new AphrontFormView()) id(new AphrontFormView())
->setWorkflow(true)
->setAction($this->getApplicationURI('update/'.$conpherence->getID().'/')) ->setAction($this->getApplicationURI('update/'.$conpherence->getID().'/'))
->setFlexible(true) ->setFlexible(true)
->setUser($user) ->setUser($user)

View file

@ -118,6 +118,27 @@ final class ConpherenceEditor extends PhabricatorApplicationTransactionEditor {
); );
} }
$editor->save(); $editor->save();
// fallthrough
case PhabricatorTransactions::TYPE_COMMENT:
$xaction_phid = $xaction->getPHID();
$behind = ConpherenceParticipationStatus::BEHIND;
$up_to_date = ConpherenceParticipationStatus::UP_TO_DATE;
$participants = $object->getParticipants();
$user = $this->getActor();
$time = time();
foreach ($participants as $phid => $participant) {
if ($phid != $user->getPHID()) {
if ($participant->getParticipationStatus() != $behind) {
$participant->setBehindTransactionPHID($xaction_phid);
}
$participant->setParticipationStatus($behind);
$participant->setDateTouched($time);
} else {
$participant->setParticipationStatus($up_to_date);
$participant->setDateTouched($time);
}
$participant->save();
}
break; break;
case ConpherenceTransactionType::TYPE_PARTICIPANTS: case ConpherenceTransactionType::TYPE_PARTICIPANTS:
foreach ($xaction->getNewValue() as $participant) { foreach ($xaction->getNewValue() as $participant) {

View file

@ -363,7 +363,7 @@ final class DifferentialRevisionListController extends DifferentialController {
array_select_keys($handles, $params['participants']), array_select_keys($handles, $params['participants']),
'getFullName'); 'getFullName');
return id(new AphrontFormTokenizerControl()) return id(new AphrontFormTokenizerControl())
->setDatasource('/typeahead/common/allmailable/') ->setDatasource('/typeahead/common/accounts/')
->setLabel($label) ->setLabel($label)
->setName('participants') ->setName('participants')
->setValue($value); ->setValue($value);

View file

@ -182,10 +182,9 @@ final class DifferentialRevisionViewController extends DifferentialController {
$warning->setSeverity(AphrontErrorView::SEVERITY_WARNING); $warning->setSeverity(AphrontErrorView::SEVERITY_WARNING);
$warning->appendChild( $warning->appendChild(
pht( pht(
'This diff is very large and affects %2$s files. Load each file '. 'This diff is very large and affects %s files. Load each file '.
'individually.', 'individually.',
$count, new PhutilNumber($count)).
PhutilTranslator::getInstance()->formatNumber($count)).
" <strong>". " <strong>".
phutil_tag( phutil_tag(
'a', 'a',
@ -499,7 +498,6 @@ final class DifferentialRevisionViewController extends DifferentialController {
'href' => "/differential/subscribe/{$action}/{$revision_id}/", 'href' => "/differential/subscribe/{$action}/{$revision_id}/",
'name' => $viewer_is_cc ? pht('Unsubscribe') : pht('Subscribe'), 'name' => $viewer_is_cc ? pht('Unsubscribe') : pht('Subscribe'),
'instant' => true, 'instant' => true,
'sigil' => 'workflow',
); );
} else { } else {
$links[] = array( $links[] = array(

View file

@ -188,6 +188,8 @@ final class DifferentialDiff extends DifferentialDAO {
'id' => $this->getID(), 'id' => $this->getID(),
'parent' => $this->getParentRevisionID(), 'parent' => $this->getParentRevisionID(),
'revisionID' => $this->getRevisionID(), 'revisionID' => $this->getRevisionID(),
'dateCreated' => $this->getDateCreated(),
'dateModified' => $this->getDateModified(),
'sourceControlBaseRevision' => $this->getSourceControlBaseRevision(), 'sourceControlBaseRevision' => $this->getSourceControlBaseRevision(),
'sourceControlPath' => $this->getSourceControlPath(), 'sourceControlPath' => $this->getSourceControlPath(),
'sourceControlSystem' => $this->getSourceControlSystem(), 'sourceControlSystem' => $this->getSourceControlSystem(),

View file

@ -229,7 +229,7 @@ final class DifferentialChangesetListView extends AphrontView {
$template = $template =
'<table><tr>'. '<table><tr>'.
'<th></th><td>%s</td>'. '<th></th><td>%s</td>'.
'<th></th><td colspan="2">%s</td>'. '<th></th><td colspan="3">%s</td>'.
'</tr></table>'; '</tr></table>';
return array( return array(

View file

@ -52,6 +52,7 @@ final class DifferentialRevisionDetailView extends AphrontView {
->setName($action['name']) ->setName($action['name'])
->setHref(idx($action, 'href')) ->setHref(idx($action, 'href'))
->setWorkflow(idx($action, 'sigil') == 'workflow') ->setWorkflow(idx($action, 'sigil') == 'workflow')
->setRenderAsForm(!empty($action['instant']))
->setUser($user) ->setUser($user)
->setDisabled(idx($action, 'disabled', false)); ->setDisabled(idx($action, 'disabled', false));
$actions->addAction($obj); $actions->addAction($obj);

View file

@ -814,9 +814,9 @@ final class DiffusionBrowseFileController extends DiffusionController {
$size = strlen($data); $size = strlen($data);
$properties->addTextContent( $properties->addTextContent(
pht('This is a binary file. It is %2$s byte(s) in length.', pht(
$size, 'This is a binary file. It is %s byte(s) in length.',
PhutilTranslator::getInstance()->formatNumber($size))); new PhutilNumber($size)));
$actions = id(new PhabricatorActionListView()) $actions = id(new PhabricatorActionListView())
->setUser($this->getRequest()->getUser()) ->setUser($this->getRequest()->getUser())

View file

@ -116,10 +116,18 @@ final class DiffusionCommitController extends DiffusionController {
$content[] = $this->buildAuditTable($commit, $audit_requests); $content[] = $this->buildAuditTable($commit, $audit_requests);
$content[] = $this->buildComments($commit); $content[] = $this->buildComments($commit);
$hard_limit = 1000;
$change_query = DiffusionPathChangeQuery::newFromDiffusionRequest( $change_query = DiffusionPathChangeQuery::newFromDiffusionRequest(
$drequest); $drequest);
$change_query->setLimit($hard_limit + 1);
$changes = $change_query->loadChanges(); $changes = $change_query->loadChanges();
$was_limited = (count($changes) > $hard_limit);
if ($was_limited) {
$changes = array_slice($changes, 0, $hard_limit);
}
$content[] = $this->buildMergesTable($commit); $content[] = $this->buildMergesTable($commit);
$owners_paths = array(); $owners_paths = array();
@ -151,7 +159,6 @@ final class DiffusionCommitController extends DiffusionController {
'r'.$callsign.$commit->getCommitIdentifier()); 'r'.$callsign.$commit->getCommitIdentifier());
} }
$pane_id = null;
if ($bad_commit) { if ($bad_commit) {
$error_panel = new AphrontErrorView(); $error_panel = new AphrontErrorView();
$error_panel->setTitle('Bad Commit'); $error_panel->setTitle('Bad Commit');
@ -175,11 +182,20 @@ final class DiffusionCommitController extends DiffusionController {
"This commit hasn't been fully parsed yet (or doesn't affect any ". "This commit hasn't been fully parsed yet (or doesn't affect any ".
"paths)."); "paths).");
$content[] = $no_changes; $content[] = $no_changes;
} else if ($was_limited) {
$huge_commit = new AphrontErrorView();
$huge_commit->setSeverity(AphrontErrorView::SEVERITY_WARNING);
$huge_commit->setTitle(pht('Enormous Commit'));
$huge_commit->appendChild(
pht(
'This commit is enormous, and affects more than %d files. '.
'Changes are not shown.',
$hard_limit));
$content[] = $huge_commit;
} else { } else {
$change_panel = new AphrontPanelView(); $change_panel = new AphrontPanelView();
$change_panel->setHeader("Changes (".number_format($count).")"); $change_panel->setHeader("Changes (".number_format($count).")");
$change_panel->setID('toc'); $change_panel->setID('toc');
if ($count > self::CHANGES_LIMIT) { if ($count > self::CHANGES_LIMIT) {
$show_all_button = phutil_tag( $show_all_button = phutil_tag(
'a', 'a',
@ -296,28 +312,11 @@ final class DiffusionCommitController extends DiffusionController {
} }
$change_table->setRenderingReferences($change_references); $change_table->setRenderingReferences($change_references);
// TODO: This is pretty awkward, unify the CSS between Diffusion and $content[] = $change_list->render();
// Differential better.
require_celerity_resource('differential-core-view-css');
$pane_id = celerity_generate_unique_node_id();
$add_comment_view = $this->renderAddCommentPanel($commit,
$audit_requests,
$pane_id);
$main_pane = phutil_render_tag(
'div',
array(
'id' => $pane_id
),
$change_list->render().
id(new PhabricatorAnchorView())
->setAnchorName('comment')
->setNavigationMarker(true)
->render().
$add_comment_view);
$content[] = $main_pane;
} }
$content[] = $this->renderAddCommentPanel($commit, $audit_requests);
$commit_id = 'r'.$callsign.$commit->getCommitIdentifier(); $commit_id = 'r'.$callsign.$commit->getCommitIdentifier();
$short_name = DiffusionView::nameCommit( $short_name = DiffusionView::nameCommit(
$repository, $repository,
@ -577,13 +576,13 @@ final class DiffusionCommitController extends DiffusionController {
private function renderAddCommentPanel( private function renderAddCommentPanel(
PhabricatorRepositoryCommit $commit, PhabricatorRepositoryCommit $commit,
array $audit_requests, array $audit_requests) {
$pane_id = null) {
assert_instances_of($audit_requests, 'PhabricatorRepositoryAuditRequest'); assert_instances_of($audit_requests, 'PhabricatorRepositoryAuditRequest');
$user = $this->getRequest()->getUser(); $user = $this->getRequest()->getUser();
$is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
$pane_id = celerity_generate_unique_node_id();
Javelin::initBehavior( Javelin::initBehavior(
'differential-keyboard-navigation', 'differential-keyboard-navigation',
array( array(
@ -693,14 +692,26 @@ final class DiffusionCommitController extends DiffusionController {
</div> </div>
</div>'; </div>';
return // TODO: This is pretty awkward, unify the CSS between Diffusion and
// Differential better.
require_celerity_resource('differential-core-view-css');
return phutil_render_tag(
'div',
array(
'id' => $pane_id,
),
phutil_render_tag( phutil_render_tag(
'div', 'div',
array( array(
'class' => 'differential-add-comment-panel', 'class' => 'differential-add-comment-panel',
), ),
id(new PhabricatorAnchorView())
->setAnchorName('comment')
->setNavigationMarker(true)
->render().
$panel->render(). $panel->render().
$preview_panel); $preview_panel));
} }
/** /**

View file

@ -3,6 +3,16 @@
final class DiffusionPathChangeQuery { final class DiffusionPathChangeQuery {
private $request; private $request;
private $limit;
public function setLimit($limit) {
$this->limit = $limit;
return $this;
}
public function getLimit() {
return $this->limit;
}
final private function __construct() { final private function __construct() {
// <private> // <private>
@ -31,20 +41,36 @@ final class DiffusionPathChangeQuery {
$commit = $drequest->loadCommit(); $commit = $drequest->loadCommit();
$conn_r = $repository->establishConnection('r');
$limit = '';
if ($this->limit) {
$limit = qsprintf(
$conn_r,
'LIMIT %d',
$this->limit + 1);
}
$raw_changes = queryfx_all( $raw_changes = queryfx_all(
$repository->establishConnection('r'), $conn_r,
'SELECT c.*, p.path pathName, t.path targetPathName, 'SELECT c.*, p.path pathName, t.path targetPathName,
i.commitIdentifier targetCommitIdentifier i.commitIdentifier targetCommitIdentifier
FROM %T c FROM %T c
LEFT JOIN %T p ON c.pathID = p.id LEFT JOIN %T p ON c.pathID = p.id
LEFT JOIN %T t ON c.targetPathID = t.id LEFT JOIN %T t ON c.targetPathID = t.id
LEFT JOIN %T i ON c.targetCommitID = i.id LEFT JOIN %T i ON c.targetCommitID = i.id
WHERE c.commitID = %d AND isDirect = 1', WHERE c.commitID = %d AND isDirect = 1 %Q',
PhabricatorRepository::TABLE_PATHCHANGE, PhabricatorRepository::TABLE_PATHCHANGE,
PhabricatorRepository::TABLE_PATH, PhabricatorRepository::TABLE_PATH,
PhabricatorRepository::TABLE_PATH, PhabricatorRepository::TABLE_PATH,
$commit->getTableName(), $commit->getTableName(),
$commit->getID()); $commit->getID(),
$limit);
$limited = $this->limit && (count($raw_changes) > $this->limit);
if ($limited) {
$raw_changes = array_slice($raw_changes, 0, $this->limit);
}
$changes = array(); $changes = array();
@ -68,20 +94,22 @@ final class DiffusionPathChangeQuery {
$changes[$id] = $change; $changes[$id] = $change;
} }
// Deduce the away paths by examining all the changes. // Deduce the away paths by examining all the changes, if we loaded them
// all.
$away = array(); if (!$limited) {
foreach ($changes as $change) { $away = array();
if ($change->getTargetPath()) { foreach ($changes as $change) {
$away[$change->getTargetPath()][] = $change->getPath(); if ($change->getTargetPath()) {
$away[$change->getTargetPath()][] = $change->getPath();
}
}
foreach ($changes as $change) {
if (isset($away[$change->getPath()])) {
$change->setAwayPaths($away[$change->getPath()]);
}
} }
} }
foreach ($changes as $change) {
if (isset($away[$change->getPath()])) {
$change->setAwayPaths($away[$change->getPath()]);
}
}
return $changes; return $changes;
} }

View file

@ -95,22 +95,21 @@ final class DiffusionBrowseTableView extends DiffusionView {
$conn = $drequest->getRepository()->establishConnection('r'); $conn = $drequest->getRepository()->establishConnection('r');
$where = ''; $path = '/'.$drequest->getPath();
$where = (substr($path, -1) == '/'
? qsprintf($conn, 'AND path LIKE %>', $path)
: qsprintf($conn, 'AND path = %s', $path));
if ($drequest->getLint()) { if ($drequest->getLint()) {
$where = qsprintf( $where .= qsprintf($conn, ' AND code = %s', $drequest->getLint());
$conn,
'AND code = %s',
$drequest->getLint());
} }
$like = (substr($drequest->getPath(), -1) == '/' ? 'LIKE %>' : '= %s');
return head(queryfx_one( return head(queryfx_one(
$conn, $conn,
'SELECT COUNT(*) FROM %T WHERE branchID = %d %Q AND path '.$like, 'SELECT COUNT(*) FROM %T WHERE branchID = %d %Q',
PhabricatorRepository::TABLE_LINTMESSAGE, PhabricatorRepository::TABLE_LINTMESSAGE,
$branch->getID(), $branch->getID(),
$where, $where));
'/'.$drequest->getPath()));
} }
public function render() { public function render() {

View file

@ -65,8 +65,6 @@ abstract class PhabricatorDirectoryController extends PhabricatorController {
continue; continue;
} }
$tile_group = msort($tile_group, 'getApplicationOrder');
$is_small_tiles = ($tile_display == PhabricatorApplication::TILE_SHOW) || $is_small_tiles = ($tile_display == PhabricatorApplication::TILE_SHOW) ||
($tile_display == PhabricatorApplication::TILE_HIDE); ($tile_display == PhabricatorApplication::TILE_HIDE);

View file

@ -44,7 +44,6 @@ final class PhabricatorApplicationDiviner extends PhabricatorApplication {
$item->setName(pht('%s Help', $application->getName())); $item->setName(pht('%s Help', $application->getName()));
$item->setIcon('help'); $item->setIcon('help');
$item->setHref($application->getHelpURI()); $item->setHref($application->getHelpURI());
$item->setSortOrder(0.1);
$items[] = $item; $items[] = $item;
} }

View file

@ -116,7 +116,7 @@ final class PhabricatorImageTransformer {
return $dst; return $dst;
} }
private function generatePreview(PhabricatorFile $file, $size) { public static function getPreviewDimensions(PhabricatorFile $file, $size) {
$data = $file->loadFileData(); $data = $file->loadFileData();
$src = imagecreatefromstring($data); $src = imagecreatefromstring($data);
@ -128,13 +128,35 @@ final class PhabricatorImageTransformer {
$dx = max($size / 4, $scale * $x); $dx = max($size / 4, $scale * $x);
$dy = max($size / 4, $scale * $y); $dy = max($size / 4, $scale * $y);
$sdx = $scale * $x;
$sdy = $scale * $y;
return array(
'x' => $x,
'y' => $y,
'dx' => $dx,
'dy' => $dy,
'sdx' => $sdx,
'sdy' => $sdy
);
}
private function generatePreview(PhabricatorFile $file, $size) {
$data = $file->loadFileData();
$src = imagecreatefromstring($data);
$dimensions = self::getPreviewDimensions($file, $size);
$x = $dimensions['x'];
$y = $dimensions['y'];
$dx = $dimensions['dx'];
$dy = $dimensions['dy'];
$sdx = $dimensions['sdx'];
$sdy = $dimensions['sdy'];
$dst = imagecreatetruecolor($dx, $dy); $dst = imagecreatetruecolor($dx, $dy);
imagesavealpha($dst, true); imagesavealpha($dst, true);
imagefill($dst, 0, 0, imagecolorallocatealpha($dst, 255, 255, 255, 127)); imagefill($dst, 0, 0, imagecolorallocatealpha($dst, 255, 255, 255, 127));
$sdx = $scale * $x;
$sdy = $scale * $y;
imagecopyresampled( imagecopyresampled(
$dst, $dst,
$src, $src,
@ -153,10 +175,14 @@ final class PhabricatorImageTransformer {
$data = $file->loadFileData(); $data = $file->loadFileData();
$img = imagecreatefromstring($data); $img = imagecreatefromstring($data);
$phabricator_root = dirname(phutil_get_library_root('phabricator')); $phabricator_root = dirname(phutil_get_library_root('phabricator'));
$font_path = $phabricator_root.'/resources/font/tuffy.ttf'; $font_root = $phabricator_root.'/resources/font/';
$white = imagecolorallocate($img, 255, 255, 255); $font_path = $font_root.'tuffy.ttf';
$black = imagecolorallocate($img, 0, 0, 0); if (Filesystem::pathExists($font_root.'impact.ttf')) {
$border_width = 3; $font_path = $font_root.'impact.ttf';
}
$text_color = imagecolorallocate($img, 255, 255, 255);
$border_color = imagecolorallocatealpha($img, 0, 0, 0, 110);
$border_width = 4;
$font_max = 200; $font_max = 200;
$font_min = 5; $font_min = 5;
for ($i = $font_max; $i > $font_min; $i--) { for ($i = $font_max; $i > $font_min; $i--) {
@ -172,8 +198,8 @@ final class PhabricatorImageTransformer {
$i, $i,
$x, $x,
$y, $y,
$white, $text_color,
$black, $border_color,
$border_width, $border_width,
$font_path, $font_path,
$upper_text); $upper_text);
@ -191,8 +217,8 @@ final class PhabricatorImageTransformer {
$i, $i,
$x, $x,
$y, $y,
$white, $text_color,
$black, $border_color,
$border_width, $border_width,
$font_path, $font_path,
$lower_text); $lower_text);

View file

@ -39,10 +39,10 @@ final class PhabricatorMacroEditController
if (!strlen($macro->getName())) { if (!strlen($macro->getName())) {
$errors[] = pht('Macro name is required.'); $errors[] = pht('Macro name is required.');
$e_name = pht('Required'); $e_name = pht('Required');
} else if (!preg_match('/^[a-z0-9_-]{3,}$/', $macro->getName())) { } else if (!preg_match('/^[a-z0-9:_-]{3,}$/', $macro->getName())) {
$errors[] = pht('Macro must be at least three characters long and '. $errors[] = pht(
'contain only lowercase letters, digits, hyphen and '. 'Macro must be at least three characters long and contain only '.
'underscore.'); 'lowercase letters, digits, hyphens, colons and underscores.');
$e_name = pht('Invalid'); $e_name = pht('Invalid');
} else { } else {
$e_name = null; $e_name = null;

View file

@ -22,8 +22,8 @@ final class PhabricatorApplicationApplications extends PhabricatorApplication {
return "\xE0\xBC\x84"; return "\xE0\xBC\x84";
} }
public function shouldAppearInLaunchView() { public function getApplicationGroup() {
return false; return self::GROUP_ADMIN;
} }
public function getRoutes() { public function getRoutes() {

View file

@ -13,15 +13,7 @@ final class PhabricatorApplicationDetailViewController
$request = $this->getRequest(); $request = $this->getRequest();
$user = $request->getUser(); $user = $request->getUser();
$selected = null; $selected = PhabricatorApplication::getByClass($this->application);
$applications = PhabricatorApplication::getAllApplications();
foreach ($applications as $application) {
if (get_class($application) == $this->application) {
$selected = $application;
break;
}
}
if (!$selected) { if (!$selected) {
return new Aphront404Response(); return new Aphront404Response();
@ -35,36 +27,53 @@ final class PhabricatorApplicationDetailViewController
->setName(pht('Applications')) ->setName(pht('Applications'))
->setHref($this->getApplicationURI())); ->setHref($this->getApplicationURI()));
$properties = $this->buildPropertyView($selected); $header = id(new PhabricatorHeaderView())
$actions = $this->buildActionView($user, $selected); ->setHeader($title);
return $this->buildApplicationPage( $status_tag = id(new PhabricatorTagView())
array( ->setType(PhabricatorTagView::TYPE_STATE);
$crumbs,
id(new PhabricatorHeaderView())->setHeader($title), if ($selected->isInstalled()) {
$actions, $status_tag->setName(pht('Installed'));
$properties, $status_tag->setBackgroundColor(PhabricatorTagView::COLOR_GREEN);
),
array( } else {
'title' => $title, $status_tag->setName(pht('Uninstalled'));
'device' => true, $status_tag->setBackgroundColor(PhabricatorTagView::COLOR_RED);
)); }
if ($selected->isBeta()) {
$beta_tag = id(new PhabricatorTagView())
->setType(PhabricatorTagView::TYPE_STATE)
->setName(pht('Beta'))
->setBackgroundColor(PhabricatorTagView::COLOR_GREY);
$header->addTag($beta_tag);
}
$header->addTag($status_tag);
$properties = $this->buildPropertyView($selected);
$actions = $this->buildActionView($user, $selected);
return $this->buildApplicationPage(
array(
$crumbs,
$header,
$actions,
$properties,
),
array(
'title' => $title,
'device' => true,
));
} }
private function buildPropertyView(PhabricatorApplication $selected) { private function buildPropertyView(PhabricatorApplication $selected) {
$properties = new PhabricatorPropertyListView(); $properties = id(new PhabricatorPropertyListView())
->addProperty(
if ($selected->isInstalled()) { pht('Description'), $selected->getShortDescription()
$properties->addProperty( );
pht('Status'), pht('Installed'));
} else {
$properties->addProperty(
pht('Status'), pht('Uninstalled'));
}
$properties->addProperty(
pht('Description'), $selected->getShortDescription());
return $properties; return $properties;
} }
@ -72,30 +81,41 @@ final class PhabricatorApplicationDetailViewController
private function buildActionView( private function buildActionView(
PhabricatorUser $user, PhabricatorApplication $selected) { PhabricatorUser $user, PhabricatorApplication $selected) {
$view = id(new PhabricatorActionListView())
->setUser($user);
if ($selected->canUninstall()) { if ($selected->canUninstall()) {
if ($selected->isInstalled()) { if ($selected->isInstalled()) {
$view->addAction(
return id(new PhabricatorActionListView()) id(new PhabricatorActionView())
->setUser($user) ->setName(pht('Uninstall'))
->addAction( ->setIcon('delete')
id(new PhabricatorActionView()) ->setWorkflow(true)
->setName(pht('Uninstall')) ->setHref(
->setIcon('delete')
->setHref(
$this->getApplicationURI(get_class($selected).'/uninstall/')) $this->getApplicationURI(get_class($selected).'/uninstall/'))
); );
} else { } else {
return id(new PhabricatorActionListView()) $view->addAction(
->setUser($user) id(new PhabricatorActionView())
->addAction( ->setName(pht('Install'))
id(new PhabricatorActionView()) ->setIcon('new')
->setName(pht('Install')) ->setWorkflow(true)
->setIcon('new') ->setHref(
->setHref( $this->getApplicationURI(get_class($selected).'/install/'))
$this->getApplicationURI(get_class($selected).'/install/')) );
);
} }
} else {
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('Uninstall'))
->setIcon('delete')
->setWorkflow(true)
->setDisabled(true)
->setHref(
$this->getApplicationURI(get_class($selected).'/uninstall/'))
);
} }
return $view;
} }
} }

View file

@ -14,30 +14,51 @@ final class PhabricatorApplicationUninstallController
public function processRequest() { public function processRequest() {
$request = $this->getRequest(); $request = $this->getRequest();
$user = $request->getUser(); $user = $request->getUser();
$app_name = substr($this->application, strlen('PhabricatorApplication'));
$selected = PhabricatorApplication::getByClass($this->application);
if (!$selected) {
return new Aphront404Response();
}
$view_uri = $this->getApplicationURI('view/'.$this->application);
if ($request->isDialogFormPost()) { if ($request->isDialogFormPost()) {
$this->manageApplication(); $this->manageApplication();
return id(new AphrontRedirectResponse())->setURI('/applications/'); return id(new AphrontRedirectResponse())->setURI($view_uri);
} }
$dialog = id(new AphrontDialogView())
->setUser($user)
->addCancelButton($view_uri);
if ($this->action == 'install') { if ($this->action == 'install') {
if ($selected->canUninstall()) {
$dialog->setTitle('Confirmation')
->appendChild(
'Install '. $selected->getName(). ' application ?'
)
->addSubmitButton('Install');
$dialog = id(new AphrontDialogView()) } else {
->setUser($user) $dialog->setTitle('Information')
->setTitle('Confirmation') ->appendChild('You cannot install a installed application.');
->appendChild('Install '. $app_name. ' application ?') }
->addSubmitButton('Install')
->addCancelButton('/applications/view/'.$this->application);
} else { } else {
$dialog = id(new AphrontDialogView()) if ($selected->canUninstall()) {
->setUser($user) $dialog->setTitle('Confirmation')
->setTitle('Confirmation') ->appendChild(
->appendChild('Really Uninstall '. $app_name. ' application ?') 'Really Uninstall '. $selected->getName(). ' application ?'
->addSubmitButton('Uninstall') )
->addCancelButton('/applications/view/'.$this->application); ->addSubmitButton('Uninstall');
} else {
$dialog->setTitle('Information')
->appendChild(
'This application cannot be uninstalled,
because it is required for Phabricator to work.'
);
}
} }
return id(new AphrontDialogResponse())->setDialog($dialog); return id(new AphrontDialogResponse())->setDialog($dialog);
} }

View file

@ -47,12 +47,24 @@ final class PhabricatorApplicationsListController
private function buildInstalledApplicationsList(array $applications) { private function buildInstalledApplicationsList(array $applications) {
$list = new PhabricatorObjectItemListView(); $list = new PhabricatorObjectItemListView();
$applications = msort($applications, 'getName');
foreach ($applications as $application) { foreach ($applications as $application) {
$item = id(new PhabricatorObjectItemView()) $item = id(new PhabricatorObjectItemView())
->setHeader($application->getName()) ->setHeader($application->getName())
->setHref('/applications/view/'.get_class($application).'/') ->setHref('/applications/view/'.get_class($application).'/')
->addAttribute($application->getShortDescription()); ->addAttribute($application->getShortDescription());
if (!$application->isInstalled()) {
$item->addIcon('delete', pht('Uninstalled'));
}
if ($application->isBeta()) {
$item->addIcon('lint-warning', pht('Beta'));
}
$list->addItem($item); $list->addItem($item);
} }
return $list; return $list;
} }

View file

@ -95,9 +95,8 @@ final class PhabricatorPasteListController extends PhabricatorPasteController {
$line_count = count(explode("\n", $paste->getContent())); $line_count = count(explode("\n", $paste->getContent()));
$line_count = pht( $line_count = pht(
'%2$s Line(s)', '%s Line(s)',
$line_count, new PhutilNumber($line_count));
PhutilTranslator::getInstance()->formatNumber($line_count));
$item = id(new PhabricatorObjectItemView()) $item = id(new PhabricatorObjectItemView())
->setHeader($paste->getFullName()) ->setHeader($paste->getFullName())

View file

@ -58,7 +58,6 @@ final class PhabricatorApplicationPeople extends PhabricatorApplication {
$item = new PhabricatorMenuItemView(); $item = new PhabricatorMenuItemView();
$item->setName($user->getUsername()); $item->setName($user->getUsername());
$item->setHref('/p/'.$user->getUsername().'/'); $item->setHref('/p/'.$user->getUsername().'/');
$item->setSortOrder(0.0);
$item->addClass('phabricator-core-menu-item-profile'); $item->addClass('phabricator-core-menu-item-profile');
$classes = array( $classes = array(

View file

@ -13,19 +13,37 @@ final class PholioMockImagesView extends AphrontView {
throw new Exception("Call setMock() before render()!"); throw new Exception("Call setMock() before render()!");
} }
$mockview = array();
$file = head($this->mock->getImages())->getFile();
$main_image_id = celerity_generate_unique_node_id(); $main_image_id = celerity_generate_unique_node_id();
require_celerity_resource('javelin-behavior-pholio-mock-view');
$config = array('mainID' => $main_image_id);
Javelin::initBehavior('pholio-mock-view', $config);
$main_image_tag = phutil_tag( $mockview = "";
$main_image = head($this->mock->getImages());
$main_image_tag = javelin_tag(
'img', 'img',
array( array(
'src' => $file->getBestURI(),
'class' => 'pholio-mock-image',
'id' => $main_image_id, 'id' => $main_image_id,
)); 'src' => $main_image->getFile()->getBestURI(),
'sigil' => 'mock-image',
'class' => 'pholio-mock-image',
'meta' => array(
'fullSizeURI' => $main_image->getFile()->getBestURI(),
'imageID' => $main_image->getID(),
),
));
$main_image_tag = javelin_tag(
'div',
array(
'id' => 'mock-wrapper',
'sigil' => 'mock-wrapper',
'class' => 'pholio-mock-wrapper'
),
$main_image_tag
);
$mockview[] = phutil_tag( $mockview[] = phutil_tag(
'div', 'div',
@ -35,10 +53,6 @@ final class PholioMockImagesView extends AphrontView {
$main_image_tag); $main_image_tag);
if (count($this->mock->getImages()) > 1) { if (count($this->mock->getImages()) > 1) {
require_celerity_resource('javelin-behavior-pholio-mock-view');
$config = array('mainID' => $main_image_id);
Javelin::initBehavior('pholio-mock-view', $config);
$thumbnails = array(); $thumbnails = array();
foreach ($this->mock->getImages() as $image) { foreach ($this->mock->getImages() as $image) {
$thumbfile = $image->getFile(); $thumbfile = $image->getFile();
@ -51,7 +65,7 @@ final class PholioMockImagesView extends AphrontView {
'class' => 'pholio-mock-carousel-thumbnail', 'class' => 'pholio-mock-carousel-thumbnail',
'meta' => array( 'meta' => array(
'fullSizeURI' => $thumbfile->getBestURI(), 'fullSizeURI' => $thumbfile->getBestURI(),
'imageID' => $image->getID(), 'imageID' => $image->getID()
), ),
)); ));
$thumbnails[] = $tag; $thumbnails[] = $tag;
@ -67,5 +81,4 @@ final class PholioMockImagesView extends AphrontView {
return $this->renderHTMLView($mockview); return $this->renderHTMLView($mockview);
} }
} }

View file

@ -266,11 +266,14 @@ final class PhabricatorRepositorySvnCommitChangeParserWorker
if (empty($raw_paths[$full_from]) && if (empty($raw_paths[$full_from]) &&
empty($effects[$full_from])) { empty($effects[$full_from])) {
if ($other_type == DifferentialChangeType::TYPE_COPY_AWAY) { if ($other_type == DifferentialChangeType::TYPE_COPY_AWAY) {
// Add an indirect effect for the copied file, if we
// don't already have an entry for it (e.g., a separate
// change).
$effects[$full_from] = array( $effects[$full_from] = array(
'rawPath' => $full_from, 'rawPath' => $full_from,
'rawTargetPath' => null, 'rawTargetPath' => null,
'rawTargetCommit' => null, 'rawTargetCommit' => null,
'rawDirect' => true, 'rawDirect' => false,
'changeType' => $other_type, 'changeType' => $other_type,
'fileType' => $from_file_type, 'fileType' => $from_file_type,

View file

@ -161,7 +161,8 @@ final class PhabricatorSearchEngineMySQL extends PhabricatorSearchEngine {
if (strlen($q)) { if (strlen($q)) {
$join[] = qsprintf( $join[] = qsprintf(
$conn_r, $conn_r,
"{$t_field} field ON field.phid = document.phid"); '%T field ON field.phid = document.phid',
$t_field);
$where[] = qsprintf( $where[] = qsprintf(
$conn_r, $conn_r,
'MATCH(corpus) AGAINST (%s IN BOOLEAN MODE)', 'MATCH(corpus) AGAINST (%s IN BOOLEAN MODE)',

View file

@ -44,7 +44,6 @@ final class PhabricatorApplicationSettings extends PhabricatorApplication {
$item->setIcon('settings'); $item->setIcon('settings');
$item->setSelected($selected); $item->setSelected($selected);
$item->setHref('/settings/'); $item->setHref('/settings/');
$item->setSortOrder(0.90);
$items[] = $item; $items[] = $item;
} }

View file

@ -7,7 +7,7 @@ arguments.
= Overview = = Overview =
Phabricator uses a lightweight markup language called "Remarkup", similar to Phabricator uses a lightweight markup language called "Remarkup", similar to
other lightweight markup langauges like Markdown and Wiki markup. other lightweight markup languages like Markdown and Wiki markup.
This document describes how to format text using Remarkup. This document describes how to format text using Remarkup.

View file

@ -35,7 +35,9 @@ final class CelerityPhabricatorResourceController
protected function buildResourceTransformer() { protected function buildResourceTransformer() {
$xformer = new CelerityResourceTransformer(); $xformer = new CelerityResourceTransformer();
$xformer->setMinify(PhabricatorEnv::getEnvConfig('celerity.minify')); $xformer->setMinify(
!PhabricatorEnv::getEnvConfig('phabricator.developer-mode') &&
PhabricatorEnv::getEnvConfig('celerity.minify'));
$xformer->setCelerityMap(CelerityResourceMap::getInstance()); $xformer->setCelerityMap(CelerityResourceMap::getInstance());
return $xformer; return $xformer;
} }

View file

@ -35,7 +35,7 @@ abstract class CelerityResourceController extends PhabricatorController {
} }
if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) &&
!PhabricatorEnv::getEnvConfig('celerity.force-disk-reads')) { !PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) {
// Return a "304 Not Modified". We don't care about the value of this // Return a "304 Not Modified". We don't care about the value of this
// field since we never change what resource is served by a given URI. // field since we never change what resource is served by a given URI.
return $this->makeResponseCacheable(new Aphront304Response()); return $this->makeResponseCacheable(new Aphront304Response());

View file

@ -74,7 +74,7 @@ final class CelerityResourceTransformer {
$bin = $root.'/externals/javelin/support/jsxmin/jsxmin'; $bin = $root.'/externals/javelin/support/jsxmin/jsxmin';
if (@file_exists($bin)) { if (@file_exists($bin)) {
$future = new ExecFuture("{$bin} __DEV__:0"); $future = new ExecFuture('%s __DEV__:0', $bin);
$future->write($data); $future->write($data);
list($err, $result) = $future->resolve(); list($err, $result) = $future->resolve();
if (!$err) { if (!$err) {

View file

@ -167,9 +167,9 @@ abstract class PhabricatorBaseEnglishTranslation
'%d Lint Messages', '%d Lint Messages',
), ),
'This is a binary file. It is %2$s byte(s) in length.' => array( 'This is a binary file. It is %s byte(s) in length.' => array(
'This is a binary file. It is %2$s byte in length.', 'This is a binary file. It is %s byte in length.',
'This is a binary file. It is %2$s bytes in length.', 'This is a binary file. It is %s bytes in length.',
), ),
'%d Action(s) Have No Effect' => array( '%d Action(s) Have No Effect' => array(
@ -226,9 +226,9 @@ abstract class PhabricatorBaseEnglishTranslation
), ),
), ),
'%2$s Line(s)' => array( '%s Line(s)' => array(
'%2$s Line', '%s Line',
'%2$s Lines', '%s Lines',
), ),
"Indexing %d object(s) of type %s." => array( "Indexing %d object(s) of type %s." => array(

View file

@ -1,5 +1,8 @@
<?php <?php
/**
* @deprecated Use javelin_tag().
*/
function javelin_render_tag( function javelin_render_tag(
$tag, $tag,
array $attributes = array(), array $attributes = array(),

View file

@ -187,9 +187,7 @@ final class PhabricatorJavelinLinter extends ArcanistLinter {
} }
private function newSymbolsFuture($path) { private function newSymbolsFuture($path) {
$javelinsymbols = 'javelinsymbols'; $future = new ExecFuture('javelinsymbols # %s', $path);
$future = new ExecFuture($javelinsymbols.' # '.escapeshellarg($path));
$future->write($this->getData($path)); $future->write($this->getData($path));
return $future; return $future;
} }

View file

@ -68,7 +68,10 @@ final class PhabricatorRemarkupRuleEmbedFile
case 'thumb': case 'thumb':
default: default:
$attrs['src'] = $file->getPreview220URI(); $attrs['src'] = $file->getPreview220URI();
$attrs['width'] = '220'; $dimensions =
PhabricatorImageTransformer::getPreviewDimensions($file, 220);
$attrs['width'] = $dimensions['sdx'];
$attrs['height'] = $dimensions['sdy'];
$options['image_class'] = 'phabricator-remarkup-embed-image'; $options['image_class'] = 'phabricator-remarkup-embed-image';
break; break;
} }

View file

@ -10,7 +10,7 @@ final class PhabricatorRemarkupRuleImageMacro
public function apply($text) { public function apply($text) {
return preg_replace_callback( return preg_replace_callback(
'@^([a-zA-Z0-9_\-]+)$@m', '@^([a-zA-Z0-9:_\-]+)$@m',
array($this, 'markupImageMacro'), array($this, 'markupImageMacro'),
$text); $text);
} }

View file

@ -121,7 +121,10 @@ final class AphrontSideNavFilterView extends AphrontView {
} }
public function addCustomBlock($block) { public function addCustomBlock($block) {
$this->menu->appendChild($block); $this->menu->addMenuItem(
id(new PhabricatorMenuItemView())
->setType(PhabricatorMenuItemView::TYPE_CUSTOM)
->appendChild($block));
return $this; return $this;
} }

View file

@ -6,13 +6,13 @@ final class PhabricatorMenuItemView extends AphrontTagView {
const TYPE_SPACER = 'type-spacer'; const TYPE_SPACER = 'type-spacer';
const TYPE_LABEL = 'type-label'; const TYPE_LABEL = 'type-label';
const TYPE_BUTTON = 'type-button'; const TYPE_BUTTON = 'type-button';
const TYPE_CUSTOM = 'type-custom';
private $name; private $name;
private $href; private $href;
private $type = self::TYPE_LINK; private $type = self::TYPE_LINK;
private $isExternal; private $isExternal;
private $key; private $key;
private $sortOrder = 1.0;
private $icon; private $icon;
private $selected; private $selected;
@ -88,15 +88,6 @@ final class PhabricatorMenuItemView extends AphrontTagView {
return $this->isExternal; return $this->isExternal;
} }
public function setSortOrder($order) {
$this->sortOrder = $order;
return $this;
}
public function getSortOrder() {
return $this->sortOrder;
}
protected function getTagName() { protected function getTagName() {
return $this->href ? 'a' : 'div'; return $this->href ? 'a' : 'div';
} }

View file

@ -4,6 +4,10 @@ final class PhabricatorMenuView extends AphrontTagView {
private $items = array(); private $items = array();
protected function canAppendChild() {
return false;
}
public function newLabel($name) { public function newLabel($name) {
$item = id(new PhabricatorMenuItemView()) $item = id(new PhabricatorMenuItemView())
->setType(PhabricatorMenuItemView::TYPE_LABEL) ->setType(PhabricatorMenuItemView::TYPE_LABEL)
@ -37,13 +41,83 @@ final class PhabricatorMenuView extends AphrontTagView {
} }
public function addMenuItem(PhabricatorMenuItemView $item) { public function addMenuItem(PhabricatorMenuItemView $item) {
$key = $item->getKey(); return $this->addMenuItemAfter(null, $item);
$this->items[] = $item; }
$this->appendChild($item);
public function addMenuItemAfter($key, PhabricatorMenuItemView $item) {
if ($key === null) {
$this->items[] = $item;
return $this;
}
if (!$this->getItem($key)) {
throw new Exception("No such key '{$key}' to add menu item after!");
}
$result = array();
foreach ($this->items as $other) {
$result[] = $other;
if ($other->getKey() == $key) {
$result[] = $item;
}
}
$this->items = $result;
return $this; return $this;
} }
public function addMenuItemBefore($key, PhabricatorMenuItemView $item) {
if ($key === null) {
array_unshift($this->items, $item);
return $this;
}
$this->requireKey($key);
$result = array();
foreach ($this->items as $other) {
if ($other->getKey() == $key) {
$result[] = $item;
}
$result[] = $other;
}
$this->items = $result;
return $this;
}
public function addMenuItemToLabel($key, PhabricatorMenuItemView $item) {
$this->requireKey($key);
$other = $this->getItem($key);
if ($other->getType() != PhabricatorMenuItemView::TYPE_LABEL) {
throw new Exception("Menu item '{$key}' is not a label!");
}
$seen = false;
$after = null;
foreach ($this->items as $other) {
if (!$seen) {
if ($other->getKey() == $key) {
$seen = true;
}
} else {
if ($other->getType() == PhabricatorMenuItemView::TYPE_LABEL) {
break;
}
}
$after = $other->getKey();
}
return $this->addMenuItemAfter($after, $item);
}
private function requireKey($key) {
if (!$this->getItem($key)) {
throw new Exception("No menu item with key '{$key}' exists!");
}
}
public function getItem($key) { public function getItem($key) {
$key = (string)$key; $key = (string)$key;
@ -83,4 +157,8 @@ final class PhabricatorMenuView extends AphrontTagView {
'class' => 'phabricator-menu-view', 'class' => 'phabricator-menu-view',
); );
} }
protected function getTagContent() {
return $this->renderSingleView($this->items);
}
} }

View file

@ -0,0 +1,141 @@
<?php
final class PhabricatorMenuViewTestCase extends PhabricatorTestCase {
public function testAppend() {
$menu = $this->newABCMenu();
$this->assertMenuKeys(
array(
'a',
'b',
'c',
),
$menu);
}
public function testAppendAfter() {
$menu = $this->newABCMenu();
$caught = null;
try {
$menu->addMenuItemAfter('x', $this->newLink('test1'));
} catch (Exception $ex) {
$caught = $ex;
}
$this->assertEqual(true, $caught instanceof Exception);
$menu->addMenuItemAfter('a', $this->newLink('test2'));
$menu->addMenuItemAfter(null, $this->newLink('test3'));
$menu->addMenuItemAfter('a', $this->newLink('test4'));
$menu->addMenuItemAfter('test3', $this->newLink('test5'));
$this->assertMenuKeys(
array(
'a',
'test4',
'test2',
'b',
'c',
'test3',
'test5',
),
$menu);
}
public function testAppendBefore() {
$menu = $this->newABCMenu();
$caught = null;
try {
$menu->addMenuItemBefore('x', $this->newLink('test1'));
} catch (Exception $ex) {
$caught = $ex;
}
$this->assertEqual(true, $caught instanceof Exception);
$menu->addMenuItemBefore('b', $this->newLink('test2'));
$menu->addMenuItemBefore(null, $this->newLink('test3'));
$menu->addMenuItemBefore('a', $this->newLink('test4'));
$menu->addMenuItemBefore('test3', $this->newLink('test5'));
$this->assertMenuKeys(
array(
'test5',
'test3',
'test4',
'a',
'test2',
'b',
'c',
),
$menu);
}
public function testAppendLabel() {
$menu = new PhabricatorMenuView();
$menu->addMenuItem($this->newLabel('fruit'));
$menu->addMenuItem($this->newLabel('animals'));
$caught = null;
try {
$menu->addMenuItemToLabel('x', $this->newLink('test1'));
} catch (Exception $ex) {
$caught = $ex;
}
$this->assertEqual(true, $caught instanceof Exception);
$menu->addMenuItemToLabel('fruit', $this->newLink('apple'));
$menu->addMenuItemToLabel('fruit', $this->newLink('banana'));
$menu->addMenuItemToLabel('animals', $this->newLink('dog'));
$menu->addMenuItemToLabel('animals', $this->newLink('cat'));
$menu->addMenuItemToLabel('fruit', $this->newLink('cherry'));
$this->assertMenuKeys(
array(
'fruit',
'apple',
'banana',
'cherry',
'animals',
'dog',
'cat',
),
$menu);
}
private function newLink($key) {
return id(new PhabricatorMenuItemView())
->setKey($key)
->setHref('#')
->setName('Link');
}
private function newLabel($key) {
return id(new PhabricatorMenuItemView())
->setType(PhabricatorMenuItemView::TYPE_LABEL)
->setKey($key)
->setName('Label');
}
private function newABCMenu() {
$menu = new PhabricatorMenuView();
$menu->addMenuItem($this->newLink('a'));
$menu->addMenuItem($this->newLink('b'));
$menu->addMenuItem($this->newLink('c'));
return $menu;
}
private function assertMenuKeys(array $expect, PhabricatorMenuView $menu) {
$items = $menu->getItems();
$keys = mpull($items, 'getKey');
$keys = array_values($keys);
$this->assertEqual($expect, $keys);
}
}

View file

@ -83,7 +83,11 @@ class PhabricatorBarePageView extends AphrontPageView {
'<script type="text/javascript">'. '<script type="text/javascript">'.
$framebust. $framebust.
'window.__DEV__=1;'. 'window.__DEV__='.
(PhabricatorEnv::getEnvConfig('phabricator.developer-mode')
? '1'
: '0').
';'.
'</script>', '</script>',
$response->renderResourcesOfType('css'), $response->renderResourcesOfType('css'),

View file

@ -256,7 +256,7 @@ final class PhabricatorStandardPageView extends PhabricatorBarePageView {
} }
$developer_warning = null; $developer_warning = null;
if (PhabricatorEnv::getEnvConfig('phabricator.show-error-callout') && if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode') &&
DarkConsoleErrorLogPluginAPI::getErrors()) { DarkConsoleErrorLogPluginAPI::getErrors()) {
$developer_warning = phutil_tag( $developer_warning = phutil_tag(
'div', 'div',

View file

@ -5,7 +5,6 @@ final class PhabricatorMainMenuIconView extends AphrontView {
private $classes = array(); private $classes = array();
private $href; private $href;
private $name; private $name;
private $sortOrder = 0.5;
private $workflow; private $workflow;
private $style; private $style;
@ -42,22 +41,6 @@ final class PhabricatorMainMenuIconView extends AphrontView {
return $this; return $this;
} }
/**
* Provide a float, where 0.0 is the profile item and 1.0 is the logout
* item. Normally you should pick something between the two.
*
* @param float Sort order.
* @return this
*/
public function setSortOrder($sort_order) {
$this->sortOrder = $sort_order;
return $this;
}
public function getSortOrder() {
return $this->sortOrder;
}
public function render() { public function render() {
$name = $this->getName(); $name = $this->getName();
$href = $this->getHref(); $href = $this->getHref();

View file

@ -164,7 +164,6 @@ final class PhabricatorMainMenuView extends AphrontView {
$controller = $this->getController(); $controller = $this->getController();
$applications = PhabricatorApplication::getAllInstalledApplications(); $applications = PhabricatorApplication::getAllInstalledApplications();
$applications = msort($applications, 'getName');
$core = array(); $core = array();
$more = array(); $more = array();
@ -184,7 +183,7 @@ final class PhabricatorMainMenuView extends AphrontView {
if ($application->getApplicationGroup() == $group_core) { if ($application->getApplicationGroup() == $group_core) {
$core[] = $item; $core[] = $item;
} else { } else {
$more[] = $item; $more[$application->getName()] = $item;
} }
} }
@ -200,7 +199,9 @@ final class PhabricatorMainMenuView extends AphrontView {
$view->addClass('phabricator-core-menu'); $view->addClass('phabricator-core-menu');
$search = $this->renderSearch(); $search = $this->renderSearch();
$view->appendChild($search); if ($search) {
$view->addMenuItem($search);
}
$view $view
->newLabel(pht('Home')) ->newLabel(pht('Home'))
@ -235,7 +236,6 @@ final class PhabricatorMainMenuView extends AphrontView {
} }
if ($actions) { if ($actions) {
$actions = msort($actions, 'getSortOrder');
$view->addMenuItem( $view->addMenuItem(
id(new PhabricatorMenuItemView()) id(new PhabricatorMenuItemView())
->addClass('phabricator-core-item-device') ->addClass('phabricator-core-item-device')
@ -260,6 +260,7 @@ final class PhabricatorMainMenuView extends AphrontView {
->addClass('phabricator-core-item-device') ->addClass('phabricator-core-item-device')
->setType(PhabricatorMenuItemView::TYPE_LABEL) ->setType(PhabricatorMenuItemView::TYPE_LABEL)
->setName(pht('More Applications'))); ->setName(pht('More Applications')));
ksort($more);
foreach ($more as $item) { foreach ($more as $item) {
$item->addClass('phabricator-core-item-device'); $item->addClass('phabricator-core-item-device');
$view->addMenuItem($item); $view->addMenuItem($item);

View file

@ -18,6 +18,19 @@
} }
.pholio-mock-image { .pholio-mock-image {
margin: 10px 0px;
display: inline-block; display: inline-block;
} }
.pholio-mock-select {
border: 1px solid #FF0000;
position: absolute;
}
.pholio-mock-wrapper {
position: relative;
display: inline-block;
cursor: crosshair;
padding: 0px;
margin: 10px 0px;
}

View file

@ -24,6 +24,10 @@
} }
.phabricator-header-tags { .phabricator-header-tags {
margin-left: 1em; margin-left: 12px;
font-size: 13px; font-size: 13px;
} }
.phabricator-header-tags .phabricator-tag-view {
margin-left: 4px;
}

View file

@ -6,13 +6,55 @@
* javelin-dom * javelin-dom
* javelin-stratcom * javelin-stratcom
* javelin-behavior-device * javelin-behavior-device
* javelin-vector
* phabricator-tooltip * phabricator-tooltip
*/ */
JX.behavior('differential-populate', function(config) { JX.behavior('differential-populate', function(config) {
function onresponse(target, response) { function onresponse(target_id, response) {
JX.DOM.replace(JX.$(target), JX.$H(response.changeset)); // As we populate the diff, we try to hold the document scroll position
// steady, so that, e.g., users who want to leave a comment on a diff with a
// large number of changes don't constantly have the text area scrolled off
// the bottom of the screen until the entire diff loads.
//
// There are two three major cases here:
//
// - If we're near the top of the document, never scroll.
// - If we're near the bottom of the document, always scroll.
// - Otherwise, scroll if the changes were above the midline of the
// viewport.
var target = JX.$(target_id);
var old_pos = JX.Vector.getScroll();
var old_view = JX.Vector.getViewport();
var old_dim = JX.Vector.getDocument();
// Number of pixels away from the top or bottom of the document which
// count as "nearby".
var sticky = 480;
var near_top = (old_pos.y <= sticky);
var near_bot = ((old_pos.y + old_view.y) >= (old_dim.y - sticky));
var target_pos = JX.Vector.getPos(target);
var target_dim = JX.Vector.getDim(target);
var target_mid = (target_pos.y + (target_dim.y / 2));
var view_mid = (old_pos.y + (old_view.y / 2));
var above_mid = (target_mid < view_mid);
JX.DOM.replace(target, JX.$H(response.changeset));
if (!near_top) {
if (near_bot || above_mid) {
// Figure out how much taller the document got.
var delta = (JX.Vector.getDocument().y - old_dim.y);
window.scrollTo(old_pos.x, old_pos.y + delta);
}
}
if (response.coverage) { if (response.coverage) {
for (var k in response.coverage) { for (var k in response.coverage) {
try { try {

View file

@ -2,8 +2,19 @@
* @provides javelin-behavior-pholio-mock-view * @provides javelin-behavior-pholio-mock-view
* @requires javelin-behavior * @requires javelin-behavior
* javelin-stratcom * javelin-stratcom
* javelin-dom
* javelin-vector
* javelin-event
*/ */
JX.behavior('pholio-mock-view', function(config) { JX.behavior('pholio-mock-view', function(config) {
var is_dragging = false;
var wrapper = JX.$('mock-wrapper');
var image;
var imageData;
var startPos;
var endPos;
var selection;
JX.Stratcom.listen( JX.Stratcom.listen(
'click', // Listen for clicks... 'click', // Listen for clicks...
'mock-thumbnail', // ...on nodes with sigil "mock-thumbnail". 'mock-thumbnail', // ...on nodes with sigil "mock-thumbnail".
@ -11,7 +22,106 @@ JX.behavior('pholio-mock-view', function(config) {
var data = e.getNodeData('mock-thumbnail'); var data = e.getNodeData('mock-thumbnail');
var main = JX.$(config.mainID); var main = JX.$(config.mainID);
JX.Stratcom.addData(
main,
{
fullSizeURI: data['fullSizeURI'],
imageID: data['imageID']
});
main.src = data.fullSizeURI; main.src = data.fullSizeURI;
JX.DOM.setContent(wrapper,main);
}); });
function draw_rectangle(node, current, init) {
JX.$V(
Math.abs(current.x-init.x),
Math.abs(current.y-init.y))
.setDim(node);
JX.$V(
(current.x-init.x < 0) ? current.x:init.x,
(current.y-init.y < 0) ? current.y:init.y)
.setPos(node);
}
function getRealXY(parent, point) {
var pos = {x: (point.x - parent.x), y: (point.y - parent.y)};
if (pos.x < 0) pos.x = 0;
else if (pos.x > image.clientWidth) pos.x = image.clientWidth - 1;
if (pos.y < 0) pos.y = 0;
else if (pos.y > image.clientHeight) pos.y = image.clientHeight - 2;
return pos;
}
JX.Stratcom.listen('mousedown', 'mock-wrapper', function(e) {
if (!e.isNormalMouseEvent()) {
return;
}
image = JX.$(config.mainID);
imageData = JX.Stratcom.getData(image);
e.getRawEvent().target.draggable = false;
is_dragging = true;
startPos = getRealXY(JX.$V(wrapper),JX.$V(e));
selection = JX.$N(
'div',
{className: 'pholio-mock-select'}
);
JX.$V(startPos.x,startPos.y).setPos(selection);
JX.DOM.appendContent(wrapper, selection);
});
JX.enableDispatch(document.body, 'mousemove');
JX.Stratcom.listen('mousemove',null, function(e) {
if (!is_dragging) {
return;
}
draw_rectangle(selection, getRealXY(JX.$V(wrapper), JX.$V(e)), startPos);
});
JX.Stratcom.listen(
'mouseup',
null,
function(e) {
if (!is_dragging) {
return;
}
is_dragging = false;
endPos = getRealXY(JX.$V(wrapper), JX.$V(e));
comment = window.prompt("Add your comment");
if (comment == null || comment == "") {
selection.remove();
return;
}
selection.title = comment;
console.log("ImageID: " + imageData['imageID'] +
", coords: (" + Math.min(startPos.x, endPos.x) + "," +
Math.min(startPos.y, endPos.y) + ") -> (" +
Math.max(startPos.x,endPos.x) + "," + Math.max(startPos.y,endPos.y) +
"), comment: " + comment);
});
}); });