1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-04 20:52:43 +01:00

(stable) Promote 2016 Week 49

This commit is contained in:
epriestley 2016-12-03 02:39:57 -08:00
commit ad65d933fa
55 changed files with 1039 additions and 260 deletions

View file

@ -9,8 +9,8 @@ return array(
'names' => array(
'conpherence.pkg.css' => '0b64e988',
'conpherence.pkg.js' => '6249a1cf',
'core.pkg.css' => '6ae56144',
'core.pkg.js' => '519f84e8',
'core.pkg.css' => '94090cab',
'core.pkg.js' => 'e4260032',
'darkconsole.pkg.js' => 'e7393ebb',
'differential.pkg.css' => 'a4ba74b5',
'differential.pkg.js' => '634399e9',
@ -108,7 +108,7 @@ return array(
'rsrc/css/application/tokens/tokens.css' => '3d0f239e',
'rsrc/css/application/uiexample/example.css' => '528b19de',
'rsrc/css/core/core.css' => 'd0801452',
'rsrc/css/core/remarkup.css' => '8e3d4635',
'rsrc/css/core/remarkup.css' => '8606d9c6',
'rsrc/css/core/syntax.css' => '769d3498',
'rsrc/css/core/z-index.css' => 'd1270942',
'rsrc/css/diviner/diviner-shared.css' => 'aa3656aa',
@ -132,7 +132,7 @@ return array(
'rsrc/css/phui/phui-chart.css' => '6bf6f78e',
'rsrc/css/phui/phui-cms.css' => 'be43c8a8',
'rsrc/css/phui/phui-comment-form.css' => '4ecc56ef',
'rsrc/css/phui/phui-comment-panel.css' => '5659325f',
'rsrc/css/phui/phui-comment-panel.css' => 'f50152ad',
'rsrc/css/phui/phui-crumbs-view.css' => '195ac419',
'rsrc/css/phui/phui-curtain-view.css' => '947bf1a4',
'rsrc/css/phui/phui-document-pro.css' => 'c354e312',
@ -141,17 +141,17 @@ return array(
'rsrc/css/phui/phui-feed-story.css' => '44a9c8e9',
'rsrc/css/phui/phui-fontkit.css' => '9cda225e',
'rsrc/css/phui/phui-form-view.css' => '3fadd537',
'rsrc/css/phui/phui-form.css' => 'b8fb087a',
'rsrc/css/phui/phui-form.css' => '2342b0e5',
'rsrc/css/phui/phui-head-thing.css' => 'fd311e5f',
'rsrc/css/phui/phui-header-view.css' => '6ec8f155',
'rsrc/css/phui/phui-hovercard.css' => 'de1a2119',
'rsrc/css/phui/phui-icon-set-selector.css' => '1ab67aad',
'rsrc/css/phui/phui-icon.css' => '417f80fb',
'rsrc/css/phui/phui-icon.css' => '09f46dd9',
'rsrc/css/phui/phui-image-mask.css' => 'a8498f9c',
'rsrc/css/phui/phui-info-panel.css' => '27ea50a1',
'rsrc/css/phui/phui-info-view.css' => 'ec92802a',
'rsrc/css/phui/phui-invisible-character-view.css' => '6993d9f0',
'rsrc/css/phui/phui-lightbox.css' => '04367b4f',
'rsrc/css/phui/phui-lightbox.css' => '0a035e40',
'rsrc/css/phui/phui-list.css' => '9da2aa00',
'rsrc/css/phui/phui-object-box.css' => '6b487c57',
'rsrc/css/phui/phui-object-item-list-view.css' => '87278fa0',
@ -166,8 +166,8 @@ return array(
'rsrc/css/phui/phui-tag-view.css' => '6bbd83e2',
'rsrc/css/phui/phui-timeline-view.css' => 'bc523970',
'rsrc/css/phui/phui-two-column-view.css' => 'bbe32c23',
'rsrc/css/phui/workboards/phui-workboard-color.css' => '207828dd',
'rsrc/css/phui/workboards/phui-workboard.css' => '60d09514',
'rsrc/css/phui/workboards/phui-workboard-color.css' => 'b60ef38a',
'rsrc/css/phui/workboards/phui-workboard.css' => '16441d5e',
'rsrc/css/phui/workboards/phui-workcard.css' => '0c62d7c5',
'rsrc/css/phui/workboards/phui-workpanel.css' => '92197373',
'rsrc/css/sprite-login.css' => '6dbbbd97',
@ -378,7 +378,7 @@ return array(
'rsrc/js/application/calendar/behavior-event-all-day.js' => 'b41537c9',
'rsrc/js/application/calendar/behavior-month-view.js' => 'fe33e256',
'rsrc/js/application/config/behavior-reorder-fields.js' => 'b6993408',
'rsrc/js/application/conpherence/ConpherenceThreadManager.js' => '358c717b',
'rsrc/js/application/conpherence/ConpherenceThreadManager.js' => 'c8b5ee6f',
'rsrc/js/application/conpherence/behavior-conpherence-search.js' => '9bbf3762',
'rsrc/js/application/conpherence/behavior-durable-column.js' => 'aa3bd034',
'rsrc/js/application/conpherence/behavior-menu.js' => '7524fcfa',
@ -505,7 +505,7 @@ return array(
'rsrc/js/core/behavior-hovercard.js' => 'bcaccd64',
'rsrc/js/core/behavior-keyboard-pager.js' => 'a8da01f0',
'rsrc/js/core/behavior-keyboard-shortcuts.js' => '01fca1f0',
'rsrc/js/core/behavior-lightbox-attachments.js' => '2674e4fa',
'rsrc/js/core/behavior-lightbox-attachments.js' => 'ddcd41cf',
'rsrc/js/core/behavior-line-linker.js' => '1499a8cb',
'rsrc/js/core/behavior-more.js' => 'a80d0378',
'rsrc/js/core/behavior-object-selector.js' => 'e0ec7f2f',
@ -566,7 +566,7 @@ return array(
'conpherence-message-pane-css' => 'b085d40d',
'conpherence-notification-css' => '965db05b',
'conpherence-participant-pane-css' => 'ac1baaa8',
'conpherence-thread-manager' => '358c717b',
'conpherence-thread-manager' => 'c8b5ee6f',
'conpherence-transaction-css' => '85129c68',
'd3' => 'a11a5ff2',
'differential-changeset-view-css' => 'b158cc46',
@ -651,7 +651,7 @@ return array(
'javelin-behavior-history-install' => '7ee2b591',
'javelin-behavior-icon-composer' => '8499b6ab',
'javelin-behavior-launch-icon-composer' => '48086888',
'javelin-behavior-lightbox-attachments' => '2674e4fa',
'javelin-behavior-lightbox-attachments' => 'ddcd41cf',
'javelin-behavior-line-chart' => 'e4232876',
'javelin-behavior-load-blame' => '42126667',
'javelin-behavior-maniphest-batch-editor' => '782ab6e7',
@ -803,7 +803,7 @@ return array(
'phabricator-object-selector-css' => '85ee8ce6',
'phabricator-phtize' => 'd254d646',
'phabricator-prefab' => '8d40ae75',
'phabricator-remarkup-css' => '8e3d4635',
'phabricator-remarkup-css' => '8606d9c6',
'phabricator-search-results-css' => '7dea472c',
'phabricator-shaped-request' => '7cbe244b',
'phabricator-slowvote-css' => 'a94b7230',
@ -847,7 +847,7 @@ return array(
'phui-chart-css' => '6bf6f78e',
'phui-cms-css' => 'be43c8a8',
'phui-comment-form-css' => '4ecc56ef',
'phui-comment-panel-css' => '5659325f',
'phui-comment-panel-css' => 'f50152ad',
'phui-crumbs-view-css' => '195ac419',
'phui-curtain-view-css' => '947bf1a4',
'phui-document-summary-view-css' => '9ca48bdf',
@ -856,20 +856,20 @@ return array(
'phui-feed-story-css' => '44a9c8e9',
'phui-font-icon-base-css' => '870a7360',
'phui-fontkit-css' => '9cda225e',
'phui-form-css' => 'b8fb087a',
'phui-form-css' => '2342b0e5',
'phui-form-view-css' => '3fadd537',
'phui-head-thing-view-css' => 'fd311e5f',
'phui-header-view-css' => '6ec8f155',
'phui-hovercard' => '1bd28176',
'phui-hovercard-view-css' => 'de1a2119',
'phui-icon-set-selector-css' => '1ab67aad',
'phui-icon-view-css' => '417f80fb',
'phui-icon-view-css' => '09f46dd9',
'phui-image-mask-css' => 'a8498f9c',
'phui-info-panel-css' => '27ea50a1',
'phui-info-view-css' => 'ec92802a',
'phui-inline-comment-view-css' => '5953c28e',
'phui-invisible-character-view-css' => '6993d9f0',
'phui-lightbox-css' => '04367b4f',
'phui-lightbox-css' => '0a035e40',
'phui-list-view-css' => '9da2aa00',
'phui-object-box-css' => '6b487c57',
'phui-object-item-list-view-css' => '87278fa0',
@ -885,8 +885,8 @@ return array(
'phui-theme-css' => '798c69b8',
'phui-timeline-view-css' => 'bc523970',
'phui-two-column-view-css' => 'bbe32c23',
'phui-workboard-color-css' => '207828dd',
'phui-workboard-view-css' => '60d09514',
'phui-workboard-color-css' => 'b60ef38a',
'phui-workboard-view-css' => '16441d5e',
'phui-workcard-view-css' => '0c62d7c5',
'phui-workpanel-view-css' => '92197373',
'phuix-action-list-view' => 'b5c256b8',
@ -1106,15 +1106,6 @@ return array(
'javelin-workflow',
'javelin-util',
),
'2674e4fa' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'javelin-mask',
'javelin-util',
'phuix-icon-view',
'phabricator-busy',
),
'2926fff2' => array(
'javelin-behavior',
'javelin-dom',
@ -1166,17 +1157,6 @@ return array(
'javelin-dom',
'javelin-workflow',
),
'358c717b' => array(
'javelin-dom',
'javelin-util',
'javelin-stratcom',
'javelin-install',
'javelin-aphlict',
'javelin-workflow',
'javelin-router',
'javelin-behavior-device',
'javelin-vector',
),
'3ab51e2c' => array(
'javelin-behavior',
'javelin-behavior-device',
@ -1361,9 +1341,6 @@ return array(
'phabricator-drag-and-drop-file-upload',
'javelin-workboard-board',
),
'5659325f' => array(
'phui-timeline-view-css',
),
'58dea2fa' => array(
'javelin-install',
'javelin-util',
@ -1975,6 +1952,17 @@ return array(
'c7ccd872' => array(
'phui-fontkit-css',
),
'c8b5ee6f' => array(
'javelin-dom',
'javelin-util',
'javelin-stratcom',
'javelin-install',
'javelin-aphlict',
'javelin-workflow',
'javelin-router',
'javelin-behavior-device',
'javelin-vector',
),
'c90a04fc' => array(
'javelin-dom',
'javelin-dynval',
@ -2059,6 +2047,15 @@ return array(
'javelin-util',
'phabricator-shaped-request',
),
'ddcd41cf' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'javelin-mask',
'javelin-util',
'phuix-icon-view',
'phabricator-busy',
),
'de2e896f' => array(
'javelin-behavior',
'javelin-dom',
@ -2177,6 +2174,9 @@ return array(
'javelin-request',
'phabricator-keyboard-shortcut',
),
'f50152ad' => array(
'phui-timeline-view-css',
),
'f6555212' => array(
'javelin-install',
'javelin-reactornode',

View file

@ -0,0 +1,3 @@
CREATE TABLE {$NAMESPACE}_search.stopwords (
value VARCHAR(32) NOT NULL COLLATE {$COLLATE_SORT}
) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_search.search_documentfield
ADD stemmedCorpus LONGTEXT COLLATE {$COLLATE_FULLTEXT};

View file

@ -0,0 +1,6 @@
CREATE TABLE {$NAMESPACE}_config.config_manualactivity (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
activityType VARCHAR(64) NOT NULL COLLATE {$COLLATE_TEXT},
parameters LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT},
UNIQUE KEY `key_type` (activityType)
) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};

View file

@ -0,0 +1,26 @@
<?php
$search_engine = PhabricatorFulltextStorageEngine::loadEngine();
$use_mysql = ($search_engine instanceof PhabricatorMySQLFulltextStorageEngine);
if ($use_mysql) {
$field = new PhabricatorSearchDocumentField();
$conn = $field->establishConnection('r');
// We're only going to require this if the index isn't empty: if you're on a
// fresh install, you don't have to do anything.
$any_documents = queryfx_one(
$conn,
'SELECT * FROM %T LIMIT 1',
$field->getTableName());
if ($any_documents) {
try {
id(new PhabricatorConfigManualActivity())
->setActivityType(PhabricatorConfigManualActivity::TYPE_REINDEX)
->save();
} catch (AphrontDuplicateKeyQueryException $ex) {
// If we've already noted that this activity is required, just move on.
}
}
}

View file

@ -67,7 +67,7 @@ then
if [ $? -ne 0 ]; then
echo "It doesn't look like you have the EPEL repo enabled. We are to add it"
echo "for you, so that we can install git."
$SUDO rpm -Uvh http://download.fedoraproject.org/pub/epel/5/i386/epel-release-5-4.noarch.rpm
$SUDO rpm -Uvh https://download.fedoraproject.org/pub/epel/5/i386/epel-release-5-4.noarch.rpm
fi
YUMCOMMAND="$SUDO yum install httpd git php53 php53-cli php53-mysql php53-process php53-devel php53-gd gcc wget make pcre-devel mysql-server"
else
@ -92,7 +92,7 @@ then
# Now that we've ensured all the devel packages required for pecl/apc are there, let's
# set up PEAR, and install apc.
echo "Attempting to install PEAR"
wget http://pear.php.net/go-pear.phar
wget https://pear.php.net/go-pear.phar
$SUDO php go-pear.phar && $SUDO pecl install apc
fi

View file

@ -1050,6 +1050,7 @@ phutil_register_library_map(array(
'FileMailReceiver' => 'applications/files/mail/FileMailReceiver.php',
'FileQueryChunksConduitAPIMethod' => 'applications/files/conduit/FileQueryChunksConduitAPIMethod.php',
'FileReplyHandler' => 'applications/files/mail/FileReplyHandler.php',
'FileTypeIcon' => 'applications/files/constants/FileTypeIcon.php',
'FileUploadChunkConduitAPIMethod' => 'applications/files/conduit/FileUploadChunkConduitAPIMethod.php',
'FileUploadConduitAPIMethod' => 'applications/files/conduit/FileUploadConduitAPIMethod.php',
'FileUploadHashConduitAPIMethod' => 'applications/files/conduit/FileUploadHashConduitAPIMethod.php',
@ -2271,11 +2272,13 @@ phutil_register_library_map(array(
'PhabricatorConfigListController' => 'applications/config/controller/PhabricatorConfigListController.php',
'PhabricatorConfigLocalSource' => 'infrastructure/env/PhabricatorConfigLocalSource.php',
'PhabricatorConfigManagementDeleteWorkflow' => 'applications/config/management/PhabricatorConfigManagementDeleteWorkflow.php',
'PhabricatorConfigManagementDoneWorkflow' => 'applications/config/management/PhabricatorConfigManagementDoneWorkflow.php',
'PhabricatorConfigManagementGetWorkflow' => 'applications/config/management/PhabricatorConfigManagementGetWorkflow.php',
'PhabricatorConfigManagementListWorkflow' => 'applications/config/management/PhabricatorConfigManagementListWorkflow.php',
'PhabricatorConfigManagementMigrateWorkflow' => 'applications/config/management/PhabricatorConfigManagementMigrateWorkflow.php',
'PhabricatorConfigManagementSetWorkflow' => 'applications/config/management/PhabricatorConfigManagementSetWorkflow.php',
'PhabricatorConfigManagementWorkflow' => 'applications/config/management/PhabricatorConfigManagementWorkflow.php',
'PhabricatorConfigManualActivity' => 'applications/config/storage/PhabricatorConfigManualActivity.php',
'PhabricatorConfigModule' => 'applications/config/module/PhabricatorConfigModule.php',
'PhabricatorConfigModuleController' => 'applications/config/controller/PhabricatorConfigModuleController.php',
'PhabricatorConfigOption' => 'applications/config/option/PhabricatorConfigOption.php',
@ -2895,6 +2898,7 @@ phutil_register_library_map(array(
'PhabricatorManiphestApplication' => 'applications/maniphest/application/PhabricatorManiphestApplication.php',
'PhabricatorManiphestConfigOptions' => 'applications/maniphest/config/PhabricatorManiphestConfigOptions.php',
'PhabricatorManiphestTaskTestDataGenerator' => 'applications/maniphest/lipsum/PhabricatorManiphestTaskTestDataGenerator.php',
'PhabricatorManualActivitySetupCheck' => 'applications/config/check/PhabricatorManualActivitySetupCheck.php',
'PhabricatorMarkupCache' => 'applications/cache/storage/PhabricatorMarkupCache.php',
'PhabricatorMarkupEngine' => 'infrastructure/markup/PhabricatorMarkupEngine.php',
'PhabricatorMarkupEngineTestCase' => 'infrastructure/markup/__tests__/PhabricatorMarkupEngineTestCase.php',
@ -5732,6 +5736,7 @@ phutil_register_library_map(array(
'FileMailReceiver' => 'PhabricatorObjectMailReceiver',
'FileQueryChunksConduitAPIMethod' => 'FileConduitAPIMethod',
'FileReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'FileTypeIcon' => 'Phobject',
'FileUploadChunkConduitAPIMethod' => 'FileConduitAPIMethod',
'FileUploadConduitAPIMethod' => 'FileConduitAPIMethod',
'FileUploadHashConduitAPIMethod' => 'FileConduitAPIMethod',
@ -7176,11 +7181,13 @@ phutil_register_library_map(array(
'PhabricatorConfigListController' => 'PhabricatorConfigController',
'PhabricatorConfigLocalSource' => 'PhabricatorConfigProxySource',
'PhabricatorConfigManagementDeleteWorkflow' => 'PhabricatorConfigManagementWorkflow',
'PhabricatorConfigManagementDoneWorkflow' => 'PhabricatorConfigManagementWorkflow',
'PhabricatorConfigManagementGetWorkflow' => 'PhabricatorConfigManagementWorkflow',
'PhabricatorConfigManagementListWorkflow' => 'PhabricatorConfigManagementWorkflow',
'PhabricatorConfigManagementMigrateWorkflow' => 'PhabricatorConfigManagementWorkflow',
'PhabricatorConfigManagementSetWorkflow' => 'PhabricatorConfigManagementWorkflow',
'PhabricatorConfigManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorConfigManualActivity' => 'PhabricatorConfigEntryDAO',
'PhabricatorConfigModule' => 'Phobject',
'PhabricatorConfigModuleController' => 'PhabricatorConfigController',
'PhabricatorConfigOption' => array(
@ -7875,6 +7882,7 @@ phutil_register_library_map(array(
'PhabricatorManiphestApplication' => 'PhabricatorApplication',
'PhabricatorManiphestConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorManiphestTaskTestDataGenerator' => 'PhabricatorTestDataGenerator',
'PhabricatorManualActivitySetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorMarkupCache' => 'PhabricatorCacheDAO',
'PhabricatorMarkupEngine' => 'Phobject',
'PhabricatorMarkupEngineTestCase' => 'PhabricatorTestCase',

View file

@ -545,6 +545,13 @@ final class AphrontRequest extends Phobject {
return id(new PhutilURI($path))->setQueryParams($get);
}
public function getAbsoluteRequestURI() {
$uri = $this->getRequestURI();
$uri->setDomain($this->getHost());
$uri->setProtocol($this->isHTTPS() ? 'https' : 'http');
return $uri;
}
public function isDialogFormPost() {
return $this->isFormPost() && $this->getStr('__dialog__');
}

View file

@ -409,19 +409,25 @@ abstract class AphrontApplicationConfiguration extends Phobject {
if (!preg_match('@/$@', $path) && $request->isHTTPGet()) {
$result = $this->routePath($maps, $path.'/');
if ($result) {
$slash_uri = $request->getRequestURI()->setPath($path.'/');
$target_uri = $request->getAbsoluteRequestURI();
// We need to restore URI encoding because the webserver has
// interpreted it. For example, this allows us to redirect a path
// like `/tag/aa%20bb` to `/tag/aa%20bb/`, which may eventually be
// resolved meaningfully by an application.
$slash_uri = phutil_escape_uri($slash_uri);
$target_path = phutil_escape_uri($path.'/');
$target_uri->setPath($target_path);
$target_uri = (string)$target_uri;
$external = strlen($request->getRequestURI()->getDomain());
return $this->buildRedirectController($slash_uri, $external);
return $this->buildRedirectController($target_uri, true);
}
}
$result = $site->new404Controller($request);
if ($result) {
return array($result, array());
}
return $this->build404Controller();
}

View file

@ -9,6 +9,10 @@ abstract class AphrontSite extends Phobject {
abstract public function newSiteForRequest(AphrontRequest $request);
abstract public function getRoutingMaps();
public function new404Controller(AphrontRequest $request) {
return null;
}
protected function isHostMatch($host, array $uris) {
foreach ($uris as $uri) {
if (!strlen($uri)) {

View file

@ -138,18 +138,54 @@ final class PhabricatorCalendarEventCancelController
->addSubmitButton($submit);
if ($show_control) {
$start_time = phutil_tag(
'strong',
array(),
phabricator_datetime($event->getStartDateTimeEpoch(), $viewer));
if ($is_cancelled) {
$this_name = pht('Reinstate Only This Event');
$this_caption = pht(
'Reinstate only the event which occurs on %s.',
$start_time);
$future_name = pht('Reinstate This And All Later Events');
$future_caption = pht(
'Reinstate this event and all events in the series which occur '.
'on or after %s.',
$start_time);
} else {
$this_name = pht('Cancel Only This Event');
$this_caption = pht(
'Cancel only the event which occurs on %s.',
$start_time);
$future_name = pht('Cancel This And All Later Events');
$future_caption = pht(
'Cancel this event and all events in the series which occur '.
'on or after %s.',
$start_time);
}
$form = id(new AphrontFormView())
->setViewer($viewer)
->appendControl(
id(new AphrontFormSelectControl())
->setLabel(pht('Cancel Events'))
id(new AphrontFormRadioButtonControl())
->setName('mode')
->setOptions(
array(
'this' => pht('Only This Event'),
'future' => pht('All Future Events'),
)));
$dialog->appendForm($form);
->setValue(PhabricatorCalendarEventEditEngine::MODE_THIS)
->addButton(
PhabricatorCalendarEventEditEngine::MODE_THIS,
$this_name,
$this_caption)
->addButton(
PhabricatorCalendarEventEditEngine::MODE_FUTURE,
$future_name,
$future_caption));
$dialog
->setWidth(AphrontDialogView::WIDTH_FORM)
->appendForm($form);
}
return $dialog;

View file

@ -46,22 +46,34 @@ final class PhabricatorCalendarEventEditController
}
if (!$mode) {
$start_time = phutil_tag(
'strong',
array(),
phabricator_datetime($event->getStartDateTimeEpoch(), $viewer));
$form = id(new AphrontFormView())
->setViewer($viewer)
->appendControl(
id(new AphrontFormSelectControl())
->setLabel(pht('Edit Events'))
id(new AphrontFormRadioButtonControl())
->setName('mode')
->setOptions(
array(
PhabricatorCalendarEventEditEngine::MODE_THIS
=> pht('Edit Only This Event'),
PhabricatorCalendarEventEditEngine::MODE_FUTURE
=> pht('Edit All Future Events'),
)));
->setValue(PhabricatorCalendarEventEditEngine::MODE_THIS)
->addButton(
PhabricatorCalendarEventEditEngine::MODE_THIS,
pht('Edit Only This Event'),
pht(
'Edit only the event which occurs at %s.',
$start_time))
->addButton(
PhabricatorCalendarEventEditEngine::MODE_FUTURE,
pht('Edit This And All Later Events'),
pht(
'Edit this event and all events in the series which '.
'occur on or after %s. This will overwrite previous '.
'edits!',
$start_time)));
return $this->newDialog()
->setTitle(pht('Edit Event'))
->setWidth(AphrontDialogView::WIDTH_FORM)
->appendParagraph(
pht(
'This event is part of a series. Which events do you '.

View file

@ -55,7 +55,7 @@ final class PhabricatorCalendarEventEditor
if ($xaction->getTransactionType() != $type_allday) {
continue;
}
$target_alllday = (bool)$xaction->getNewValue();
$new_allday = (bool)$xaction->getNewValue();
}
$this->oldIsAllDay = $old_allday;

View file

@ -1024,6 +1024,10 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
$set = new PhutilCalendarRecurrenceSet();
if ($this->viewerTimezone) {
$set->setViewerTimezone($this->viewerTimezone);
}
$rrule = $this->newRecurrenceRule();
if (!$rrule) {
return null;

View file

@ -22,6 +22,35 @@ abstract class PhabricatorCalendarEventDateTransaction
->toDictionary();
}
public function getTransactionHasEffect($object, $old, $new) {
$editor = $this->getEditor();
$actor = $this->getActor();
$actor_timezone = $actor->getTimezoneIdentifier();
// When an edit only changes the timezone of an event without materially
// changing the absolute time, discard it. This can happen if two users in
// different timezones edit an event without rescheduling it.
// Eventually, after T11073, there may be a UI control to adjust timezones.
// If a user explicitly changed the timezone, we should respect that.
// However, there is no way for users to intentionally apply this kind of
// edit today.
$old_datetime = PhutilCalendarAbsoluteDateTime::newFromDictionary($old)
->setIsAllDay($editor->getNewIsAllDay())
->setViewerTimezone($actor_timezone);
$new_datetime = PhutilCalendarAbsoluteDateTime::newFromDictionary($new)
->setIsAllDay($editor->getNewIsAllDay())
->setViewerTimezone($actor_timezone);
$old_epoch = $old_datetime->getEpoch();
$new_epoch = $new_datetime->getEpoch();
return ($old_epoch !== $new_epoch);
}
public function validateTransactions($object, array $xactions) {
$errors = array();

View file

@ -0,0 +1,75 @@
<?php
final class PhabricatorManualActivitySetupCheck
extends PhabricatorSetupCheck {
public function getDefaultGroup() {
return self::GROUP_OTHER;
}
protected function executeChecks() {
$activities = id(new PhabricatorConfigManualActivity())->loadAll();
foreach ($activities as $activity) {
$type = $activity->getActivityType();
// For now, there is only one type of manual activity. It's not clear
// if we're really going to have too much more of this stuff so this
// is a bit under-designed for now.
$activity_name = pht('Rebuild Search Index');
$activity_summary = pht(
'The search index algorithm has been updated and the index needs '.
'be rebuilt.');
$message = array();
$message[] = pht(
'The indexing algorithm for the fulltext search index has been '.
'updated and the index needs to be rebuilt. Until you rebuild the '.
'index, global search (and other fulltext search) will not '.
'function correctly.');
$message[] = pht(
'You can rebuild the search index while Phabricator is running.');
$message[] = pht(
'To rebuild the index, run this command:');
$message[] = phutil_tag(
'pre',
array(),
(string)csprintf(
'phabricator/ $ ./bin/search index --all --force --background'));
$message[] = pht(
'You can find more information about rebuilding the search '.
'index here: %s',
phutil_tag(
'a',
array(
'href' => 'https://phurl.io/u/reindex',
'target' => '_blank',
),
'https://phurl.io/u/reindex'));
$message[] = pht(
'After rebuilding the index, run this command to clear this setup '.
'warning:');
$message[] = phutil_tag(
'pre',
array(),
(string)csprintf('phabricator/ $ ./bin/config done %R', $type));
$activity_message = phutil_implode_html("\n\n", $message);
$this->newIssue('manual.'.$type)
->setName($activity_name)
->setSummary($activity_summary)
->setMessage($activity_message);
}
}
}

View file

@ -121,8 +121,18 @@ final class PhabricatorMySQLSetupCheck extends PhabricatorSetupCheck {
->addMySQLConfig('sql_mode');
}
$stopword_file = $ref->loadRawMySQLConfigValue('ft_stopword_file');
$is_innodb_fulltext = false;
$is_myisam_fulltext = false;
if ($this->shouldUseMySQLSearchEngine()) {
if (PhabricatorSearchDocument::isInnoDBFulltextEngineAvailable()) {
$is_innodb_fulltext = true;
} else {
$is_myisam_fulltext = true;
}
}
if ($is_myisam_fulltext) {
$stopword_file = $ref->loadRawMySQLConfigValue('ft_stopword_file');
if ($stopword_file === null) {
$summary = pht(
'Your version of MySQL (on database host "%s") does not support '.
@ -200,9 +210,9 @@ final class PhabricatorMySQLSetupCheck extends PhabricatorSetupCheck {
}
}
if ($is_myisam_fulltext) {
$min_len = $ref->loadRawMySQLConfigValue('ft_min_word_len');
if ($min_len >= 4) {
if ($this->shouldUseMySQLSearchEngine()) {
$namespace = PhabricatorEnv::getEnvConfig('storage.default-namespace');
$summary = pht(
@ -248,6 +258,18 @@ final class PhabricatorMySQLSetupCheck extends PhabricatorSetupCheck {
}
}
// NOTE: The default value of "innodb_ft_min_token_size" is 3, which is
// a reasonable value, so we do not warn about it: if it is set to
// something else, the user adjusted it on their own.
// NOTE: We populate a stopwords table at "phabricator_search.stopwords",
// but the default InnoDB stopword list is pretty reasonable (36 words,
// versus 500+ in MyISAM). Just use the builtin list until we run into
// concrete issues with it. Users can switch to our stopword table with:
//
// [mysqld]
// innodb_ft_server_stopword_table = phabricator_search/stopwords
$innodb_pool = $ref->loadRawMySQLConfigValue('innodb_buffer_pool_size');
$innodb_bytes = phutil_parse_bytes($innodb_pool);
$innodb_readable = phutil_format_bytes($innodb_bytes);

View file

@ -0,0 +1,71 @@
<?php
final class PhabricatorConfigManagementDoneWorkflow
extends PhabricatorConfigManagementWorkflow {
protected function didConstruct() {
$this
->setName('done')
->setExamples('**done** __activity__')
->setSynopsis(pht('Mark a manual upgrade activity as complete.'))
->setArguments(
array(
array(
'name' => 'force',
'short' => 'f',
'help' => pht(
'Mark activities complete even if there is no outstanding '.
'need to complete them.'),
),
array(
'name' => 'activities',
'wildcard' => true,
),
));
}
public function execute(PhutilArgumentParser $args) {
$is_force = $args->getArg('force');
$activities = $args->getArg('activities');
if (!$activities) {
throw new PhutilArgumentUsageException(
pht('Specify an activity to mark as completed.'));
}
foreach ($activities as $type) {
$activity = id(new PhabricatorConfigManualActivity())->loadOneWhere(
'activityType = %s',
$type);
if (!$activity) {
if ($is_force) {
echo tsprintf(
"%s\n",
pht(
'Activity "%s" did not need to be marked as complete.',
$type));
} else {
throw new PhutilArgumentUsageException(
pht(
'Activity "%s" is not currently marked as required, so there '.
'is no need to complete it.',
$type));
}
} else {
$activity->delete();
echo tsprintf(
"%s\n",
pht(
'Marked activity "%s" as completed.',
$type));
}
}
echo tsprintf(
"%s\n",
pht('Done.'));
return 0;
}
}

View file

@ -68,7 +68,7 @@ final class PhabricatorConfigSchemaQuery extends Phobject {
$tables = queryfx_all(
$conn,
'SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_COLLATION
'SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_COLLATION, ENGINE
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA IN (%Ls)',
$databases);
@ -146,7 +146,8 @@ final class PhabricatorConfigSchemaQuery extends Phobject {
$table_schema = id(new PhabricatorConfigTableSchema())
->setName($table_name)
->setCollation($table['TABLE_COLLATION']);
->setCollation($table['TABLE_COLLATION'])
->setEngine($table['ENGINE']);
$columns = idx($database_column_info, $table_name, array());
foreach ($columns as $column) {

View file

@ -64,6 +64,12 @@ abstract class PhabricatorConfigSchemaSpec extends Phobject {
$table = $this->newTable($table_name);
if (PhabricatorSearchDocument::isInnoDBFulltextEngineAvailable()) {
$fulltext_engine = 'InnoDB';
} else {
$fulltext_engine = 'MyISAM';
}
foreach ($columns as $name => $type) {
if ($type === null) {
continue;
@ -85,6 +91,15 @@ abstract class PhabricatorConfigSchemaSpec extends Phobject {
->setNullable($nullable)
->setAutoIncrement($auto);
// If this table has any FULLTEXT fields, we expect it to use the best
// available FULLTEXT engine, which may not be InnoDB.
switch ($type) {
case 'fulltext':
case 'fulltext?':
$table->setEngine($fulltext_engine);
break;
}
$table->addColumn($column);
}
@ -174,7 +189,8 @@ abstract class PhabricatorConfigSchemaSpec extends Phobject {
protected function newTable($name) {
return id(new PhabricatorConfigTableSchema())
->setName($name)
->setCollation($this->getUTF8BinaryCollation());
->setCollation($this->getUTF8BinaryCollation())
->setEngine('InnoDB');
}
protected function newColumn($name) {

View file

@ -18,6 +18,7 @@ abstract class PhabricatorConfigStorageSchema extends Phobject {
const ISSUE_AUTOINCREMENT = 'autoincrement';
const ISSUE_UNKNOWN = 'unknown';
const ISSUE_ACCESSDENIED = 'accessdenied';
const ISSUE_ENGINE = 'engine';
const STATUS_OKAY = 'okay';
const STATUS_WARN = 'warn';
@ -133,6 +134,8 @@ abstract class PhabricatorConfigStorageSchema extends Phobject {
return pht('Column Has No Specification');
case self::ISSUE_ACCESSDENIED:
return pht('Access Denied');
case self::ISSUE_ENGINE:
return pht('Better Table Engine Available');
default:
throw new Exception(pht('Unknown schema issue "%s"!', $issue));
}
@ -170,6 +173,8 @@ abstract class PhabricatorConfigStorageSchema extends Phobject {
return pht('This column has the wrong autoincrement setting.');
case self::ISSUE_UNKNOWN:
return pht('This column is missing a type specification.');
case self::ISSUE_ENGINE:
return pht('This table can use a better table engine.');
default:
throw new Exception(pht('Unknown schema issue "%s"!', $issue));
}
@ -194,6 +199,7 @@ abstract class PhabricatorConfigStorageSchema extends Phobject {
case self::ISSUE_KEYCOLUMNS:
case self::ISSUE_LONGKEY:
case self::ISSUE_AUTOINCREMENT:
case self::ISSUE_ENGINE:
return self::STATUS_WARN;
default:
throw new Exception(pht('Unknown schema issue "%s"!', $issue));

View file

@ -4,6 +4,7 @@ final class PhabricatorConfigTableSchema
extends PhabricatorConfigStorageSchema {
private $collation;
private $engine;
private $columns = array();
private $keys = array();
@ -62,6 +63,15 @@ final class PhabricatorConfigTableSchema
return $this->collation;
}
public function setEngine($engine) {
$this->engine = $engine;
return $this;
}
public function getEngine() {
return $this->engine;
}
protected function compareToSimilarSchema(
PhabricatorConfigStorageSchema $expect) {
@ -70,6 +80,10 @@ final class PhabricatorConfigTableSchema
$issues[] = self::ISSUE_COLLATION;
}
if ($this->getEngine() != $expect->getEngine()) {
$issues[] = self::ISSUE_ENGINE;
}
return $issues;
}

View file

@ -0,0 +1,38 @@
<?php
final class PhabricatorConfigManualActivity
extends PhabricatorConfigEntryDAO {
protected $activityType;
protected $parameters = array();
const TYPE_REINDEX = 'reindex';
protected function getConfiguration() {
return array(
self::CONFIG_TIMESTAMPS => false,
self::CONFIG_SERIALIZATION => array(
'parameters' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'activityType' => 'text64',
),
self::CONFIG_KEY_SCHEMA => array(
'key_type' => array(
'columns' => array('activityType'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function setParameter($key, $value) {
$this->parameters[$key] = $value;
return $this;
}
public function getParameter($key, $default = null) {
return idx($this->parameters, $key, $default);
}
}

View file

@ -110,67 +110,6 @@ final class DifferentialChangesetDetailView extends AphrontView {
return $this->vsChangesetID;
}
public function getFileIcon($filename) {
$path_info = pathinfo($filename);
$extension = idx($path_info, 'extension');
switch ($extension) {
case 'psd':
case 'ai':
$icon = 'fa-eye';
break;
case 'conf':
$icon = 'fa-wrench';
break;
case 'wav':
case 'mp3':
case 'aiff':
$icon = 'fa-file-sound-o';
break;
case 'm4v':
case 'mov':
$icon = 'fa-file-movie-o';
break;
case 'sql':
case 'db':
$icon = 'fa-database';
break;
case 'xls':
case 'csv':
$icon = 'fa-file-excel-o';
break;
case 'ics':
$icon = 'fa-calendar';
break;
case 'zip':
case 'tar':
case 'bz':
case 'tgz':
case 'gz':
$icon = 'fa-file-archive-o';
break;
case 'png':
case 'jpg':
case 'bmp':
case 'gif':
$icon = 'fa-file-picture-o';
break;
case 'txt':
$icon = 'fa-file-text-o';
break;
case 'doc':
case 'docx':
$icon = 'fa-file-word-o';
break;
case 'pdf':
$icon = 'fa-file-pdf-o';
break;
default:
$icon = 'fa-file-code-o';
break;
}
return $icon;
}
public function render() {
$this->requireResource('differential-changeset-view-css');
$this->requireResource('syntax-highlighting-css');
@ -204,7 +143,7 @@ final class DifferentialChangesetDetailView extends AphrontView {
}
$display_filename = $changeset->getDisplayFilename();
$display_icon = $this->getFileIcon($display_filename);
$display_icon = FileTypeIcon::getFileIcon($display_filename);
$icon = id(new PHUIIconView())
->setIcon($display_icon);

View file

@ -462,6 +462,11 @@ final class DiffusionURIEditor
->withRepositories(array($repository))
->execute();
// Reattach the current URIs to the repository: we're going to rebuild
// the index explicitly below, and want to include any changes made to
// this URI in the index update.
$repository->attachURIs($uris);
$observe_uri = null;
foreach ($uris as $uri) {
if ($uri->getIoType() != PhabricatorRepositoryURI::IO_OBSERVE) {
@ -488,6 +493,9 @@ final class DiffusionURIEditor
$repository->save();
// Explicitly update the URI index.
$repository->updateURIIndex();
$is_hosted = $repository->isHosted();
// If we've swapped the repository from hosted to observed or vice versa,

View file

@ -35,4 +35,14 @@ abstract class DiffusionGitSSHWorkflow
}
}
protected function raiseWrongVCSException(
PhabricatorRepository $repository) {
throw new Exception(
pht(
'This repository ("%s") is not a Git repository. Use "%s" to '.
'interact with this repository.',
$repository->getDisplayName(),
$repository->getVersionControlSystem()));
}
}

View file

@ -119,4 +119,14 @@ final class DiffusionMercurialServeSSHWorkflow
return $raw_message;
}
protected function raiseWrongVCSException(
PhabricatorRepository $repository) {
throw new Exception(
pht(
'This repository ("%s") is not a Mercurial repository. Use "%s" to '.
'interact with this repository.',
$repository->getDisplayName(),
$repository->getVersionControlSystem()));
}
}

View file

@ -43,6 +43,8 @@ abstract class DiffusionSSHWorkflow extends PhabricatorSSHWorkflow {
*/
abstract protected function identifyRepository();
abstract protected function executeRepositoryOperations();
abstract protected function raiseWrongVCSException(
PhabricatorRepository $repository);
protected function getBaseRequestPath() {
return $this->baseRequestPath;
@ -199,6 +201,10 @@ abstract class DiffusionSSHWorkflow extends PhabricatorSSHWorkflow {
$repository->getDisplayName()));
}
if ($repository->getVersionControlSystem() != $vcs) {
$this->raiseWrongVCSException($repository);
}
return $repository;
}

View file

@ -449,4 +449,14 @@ final class DiffusionSubversionServeSSHWorkflow
return $path;
}
protected function raiseWrongVCSException(
PhabricatorRepository $repository) {
throw new Exception(
pht(
'This repository ("%s") is not a Subversion repository. Use "%s" to '.
'interact with this repository.',
$repository->getDisplayName(),
$repository->getVersionControlSystem()));
}
}

View file

@ -0,0 +1,66 @@
<?php
final class FileTypeIcon extends Phobject {
public static function getFileIcon($filename) {
$path_info = pathinfo($filename);
$extension = idx($path_info, 'extension');
switch ($extension) {
case 'psd':
case 'ai':
$icon = 'fa-file-image-o';
break;
case 'conf':
$icon = 'fa-wrench';
break;
case 'wav':
case 'mp3':
case 'aiff':
$icon = 'fa-file-sound-o';
break;
case 'm4v':
case 'mov':
$icon = 'fa-file-movie-o';
break;
case 'sql':
case 'db':
$icon = 'fa-database';
break;
case 'xls':
case 'csv':
$icon = 'fa-file-excel-o';
break;
case 'ics':
$icon = 'fa-calendar';
break;
case 'zip':
case 'tar':
case 'bz':
case 'tgz':
case 'gz':
$icon = 'fa-file-archive-o';
break;
case 'png':
case 'jpg':
case 'bmp':
case 'gif':
$icon = 'fa-file-picture-o';
break;
case 'txt':
$icon = 'fa-file-text-o';
break;
case 'doc':
case 'docx':
$icon = 'fa-file-word-o';
break;
case 'pdf':
$icon = 'fa-file-pdf-o';
break;
default:
$icon = 'fa-file-text-o';
break;
}
return $icon;
}
}

View file

@ -287,6 +287,7 @@ final class PhabricatorEmbedFileRemarkupRule
->setFileDownloadURI($file->getDownloadURI())
->setFileViewURI($file->getBestURI())
->setFileViewable((bool)$options['viewable'])
->setFileSize(phutil_format_bytes($file->getByteSize()))
->setFileMonogram($file->getMonogram());
}

View file

@ -92,7 +92,6 @@ final class PhabricatorPhameApplication extends PhabricatorApplication {
'/' => array(
'' => 'PhameBlogViewController',
'post/(?P<id>\d+)/(?:(?P<slug>[^/]+)/)?' => 'PhamePostViewController',
'.*' => 'PhameBlog404Controller',
),
);

View file

@ -60,6 +60,10 @@ final class PhameBlogSite extends PhameSite {
return id(new PhameBlogSite())->setBlog($blog);
}
public function new404Controller(AphrontRequest $request) {
return new PhameBlog404Controller();
}
public function getRoutingMaps() {
$app = PhabricatorApplication::getByClass('PhabricatorPhameApplication');

View file

@ -416,6 +416,8 @@ final class PhabricatorProjectBoardViewController
->appendChild($board)
->addClass('project-board-wrapper');
$nav = $this->getProfileMenu();
$divider = id(new PHUIListItemView())
->setType(PHUIListItemView::TYPE_DIVIDER);
$fullscreen = $this->buildFullscreenMenu();
@ -438,6 +440,7 @@ final class PhabricatorProjectBoardViewController
))
->setPageObjectPHIDs(array($project->getPHID()))
->setShowFooter(false)
->setNavigation($nav)
->setCrumbs($crumbs)
->addQuicksandConfig(
array(

View file

@ -33,6 +33,8 @@ final class PhabricatorMySQLFulltextStorageEngine
$conn_w = $store->establishConnection('w');
$stemmer = new PhutilSearchStemmer();
$field_dao = new PhabricatorSearchDocumentField();
queryfx(
$conn_w,
@ -41,16 +43,21 @@ final class PhabricatorMySQLFulltextStorageEngine
$phid);
foreach ($doc->getFieldData() as $field) {
list($ftype, $corpus, $aux_phid) = $field;
$stemmed_corpus = $stemmer->stemCorpus($corpus);
queryfx(
$conn_w,
'INSERT INTO %T (phid, phidType, field, auxPHID, corpus) '.
'VALUES (%s, %s, %s, %ns, %s)',
'INSERT INTO %T
(phid, phidType, field, auxPHID, corpus, stemmedCorpus) '.
'VALUES (%s, %s, %s, %ns, %s, %s)',
$field_dao->getTableName(),
$phid,
$doc->getDocumentType(),
$ftype,
$aux_phid,
$corpus);
$corpus,
$stemmed_corpus);
}
@ -153,71 +160,112 @@ final class PhabricatorMySQLFulltextStorageEngine
}
public function executeSearch(PhabricatorSavedQuery $query) {
$where = array();
$table = new PhabricatorSearchDocument();
$document_table = $table->getTableName();
$conn = $table->establishConnection('r');
$subquery = $this->newFulltextSubquery($query, $conn);
$offset = (int)$query->getParameter('offset', 0);
$limit = (int)$query->getParameter('limit', 25);
// NOTE: We must JOIN the subquery in order to apply a limit.
$results = queryfx_all(
$conn,
'SELECT
documentPHID,
MAX(fieldScore) AS documentScore
FROM (%Q) query
JOIN %T root ON query.documentPHID = root.phid
GROUP BY documentPHID
ORDER BY documentScore DESC
LIMIT %d, %d',
$subquery,
$document_table,
$offset,
$limit);
return ipull($results, 'documentPHID');
}
private function newFulltextSubquery(
PhabricatorSavedQuery $query,
AphrontDatabaseConnection $conn) {
$field = new PhabricatorSearchDocumentField();
$field_table = $field->getTableName();
$document = new PhabricatorSearchDocument();
$document_table = $document->getTableName();
$select = array();
$select[] = 'document.phid AS documentPHID';
$join = array();
$order = 'ORDER BY documentCreated DESC';
$where = array();
$dao_doc = new PhabricatorSearchDocument();
$dao_field = new PhabricatorSearchDocumentField();
$t_doc = $dao_doc->getTableName();
$t_field = $dao_field->getTableName();
$conn_r = $dao_doc->establishConnection('r');
$title_field = PhabricatorSearchDocumentFieldType::FIELD_TITLE;
$title_boost = 1024;
$raw_query = $query->getParameter('query');
$q = $this->compileQuery($raw_query);
$compiled_query = $this->compileQuery($raw_query);
if (strlen($compiled_query)) {
$select[] = qsprintf(
$conn,
'IF(field.field = %s, %d, 0) +
MATCH(corpus, stemmedCorpus) AGAINST (%s IN BOOLEAN MODE)
AS fieldScore',
$title_field,
$title_boost,
$compiled_query);
if (strlen($q)) {
$join[] = qsprintf(
$conn_r,
$conn,
'%T field ON field.phid = document.phid',
$t_field);
$where[] = qsprintf(
$conn_r,
'MATCH(corpus) AGAINST (%s IN BOOLEAN MODE)',
$q);
$field_table);
// When searching for a string, promote user listings above other
// listings.
$order = qsprintf(
$conn_r,
'ORDER BY
IF(documentType = %s, 0, 1) ASC,
MAX(MATCH(corpus) AGAINST (%s)) DESC',
'USER',
$q);
$field = $query->getParameter('field');
if ($field) {
$where[] = qsprintf(
$conn_r,
$conn,
'MATCH(corpus, stemmedCorpus) AGAINST (%s IN BOOLEAN MODE)',
$compiled_query);
if ($query->getParameter('field')) {
$where[] = qsprintf(
$conn,
'field.field = %s',
$field);
}
} else {
$select[] = qsprintf(
$conn,
'document.documentCreated AS fieldScore');
}
$exclude = $query->getParameter('exclude');
if ($exclude) {
$where[] = qsprintf($conn_r, 'document.phid != %s', $exclude);
$where[] = qsprintf(
$conn,
'document.phid != %s',
$exclude);
}
$types = $query->getParameter('types');
if ($types) {
if (strlen($q)) {
if (strlen($compiled_query)) {
$where[] = qsprintf(
$conn_r,
$conn,
'field.phidType IN (%Ls)',
$types);
}
$where[] = qsprintf(
$conn_r,
$conn,
'document.documentType IN (%Ls)',
$types);
}
$join[] = $this->joinRelationship(
$conn_r,
$conn,
$query,
'authorPHIDs',
PhabricatorSearchRelationship::RELATIONSHIP_AUTHOR);
@ -231,14 +279,14 @@ final class PhabricatorMySQLFulltextStorageEngine
if ($include_open && !$include_closed) {
$join[] = $this->joinRelationship(
$conn_r,
$conn,
$query,
'statuses',
$open_rel,
true);
} else if ($include_closed && !$include_open) {
$join[] = $this->joinRelationship(
$conn_r,
$conn,
$query,
'statuses',
$closed_rel,
@ -247,46 +295,47 @@ final class PhabricatorMySQLFulltextStorageEngine
if ($query->getParameter('withAnyOwner')) {
$join[] = $this->joinRelationship(
$conn_r,
$conn,
$query,
'withAnyOwner',
PhabricatorSearchRelationship::RELATIONSHIP_OWNER,
true);
} else if ($query->getParameter('withUnowned')) {
$join[] = $this->joinRelationship(
$conn_r,
$conn,
$query,
'withUnowned',
PhabricatorSearchRelationship::RELATIONSHIP_UNOWNED,
true);
} else {
$join[] = $this->joinRelationship(
$conn_r,
$conn,
$query,
'ownerPHIDs',
PhabricatorSearchRelationship::RELATIONSHIP_OWNER);
}
$join[] = $this->joinRelationship(
$conn_r,
$conn,
$query,
'subscriberPHIDs',
PhabricatorSearchRelationship::RELATIONSHIP_SUBSCRIBER);
$join[] = $this->joinRelationship(
$conn_r,
$conn,
$query,
'projectPHIDs',
PhabricatorSearchRelationship::RELATIONSHIP_PROJECT);
$join[] = $this->joinRelationship(
$conn_r,
$conn,
$query,
'repository',
PhabricatorSearchRelationship::RELATIONSHIP_REPOSITORY);
$join = array_filter($join);
$select = implode(', ', $select);
$join = array_filter($join);
foreach ($join as $key => $clause) {
$join[$key] = ' JOIN '.$clause;
}
@ -298,27 +347,24 @@ final class PhabricatorMySQLFulltextStorageEngine
$where = '';
}
$offset = (int)$query->getParameter('offset', 0);
$limit = (int)$query->getParameter('limit', 25);
if (strlen($compiled_query)) {
$order = '';
} else {
// When not executing a query, order by document creation date. This
// is the default view in object browser dialogs, like "Close Duplicate".
$order = qsprintf(
$conn,
'ORDER BY document.documentCreated DESC');
}
$hits = queryfx_all(
$conn_r,
'SELECT
document.phid
FROM %T document
%Q
%Q
GROUP BY document.phid
%Q
LIMIT %d, %d',
$t_doc,
return qsprintf(
$conn,
'SELECT %Q FROM %T document %Q %Q %Q LIMIT 1000',
$select,
$document_table,
$join,
$where,
$order,
$offset,
$limit);
return ipull($hits, 'phid');
$order);
}
protected function joinRelationship(
@ -353,11 +399,17 @@ final class PhabricatorMySQLFulltextStorageEngine
}
private function compileQuery($raw_query) {
$compiler = PhabricatorSearchDocument::newQueryCompiler();
$stemmer = new PhutilSearchStemmer();
return $compiler
$compiler = PhabricatorSearchDocument::newQueryCompiler()
->setQuery($raw_query)
->compileQuery();
->setStemmer($stemmer);
$queries = array();
$queries[] = $compiler->compileLiteralQuery();
$queries[] = $compiler->compileStemmedQuery();
return implode(' ', array_filter($queries));
}
public function indexExists() {

View file

@ -5,6 +5,14 @@ final class PhabricatorSearchSchemaSpec
public function buildSchemata() {
$this->buildEdgeSchemata(new PhabricatorProfilePanelConfiguration());
$this->buildRawSchema(
'search',
PhabricatorSearchDocument::STOPWORDS_TABLE,
array(
'value' => 'sort32',
),
array());
}
}

View file

@ -7,6 +7,8 @@ final class PhabricatorSearchDocument extends PhabricatorSearchDAO {
protected $documentCreated;
protected $documentModified;
const STOPWORDS_TABLE = 'stopwords';
protected function getConfiguration() {
return array(
self::CONFIG_TIMESTAMPS => false,
@ -38,19 +40,46 @@ final class PhabricatorSearchDocument extends PhabricatorSearchDAO {
}
public static function newQueryCompiler() {
$compiler = new PhutilSearchQueryCompiler();
if (self::isInnoDBFulltextEngineAvailable()) {
// The InnoDB fulltext boolean operators are always the same as the
// default MyISAM operators, so we do not need to adjust the compiler.
} else {
$table = new self();
$conn = $table->establishConnection('r');
$compiler = new PhutilSearchQueryCompiler();
$operators = queryfx_one(
$conn,
'SELECT @@ft_boolean_syntax AS syntax');
if ($operators) {
$compiler->setOperators($operators['syntax']);
}
}
return $compiler;
}
public static function isInnoDBFulltextEngineAvailable() {
static $available;
if ($available === null) {
$table = new self();
$conn = $table->establishConnection('r');
// If this system variable exists, we can use InnoDB fulltext. If it
// does not, this query will throw and we're stuck with MyISAM.
try {
queryfx_one(
$conn,
'SELECT @@innodb_ft_max_token_size');
$available = true;
} catch (AphrontQueryException $x) {
$available = false;
}
}
return $available;
}
}

View file

@ -6,6 +6,7 @@ final class PhabricatorSearchDocumentField extends PhabricatorSearchDAO {
protected $field;
protected $auxPHID;
protected $corpus;
protected $stemmedCorpus;
protected function getConfiguration() {
return array(
@ -16,14 +17,15 @@ final class PhabricatorSearchDocumentField extends PhabricatorSearchDAO {
'field' => 'text4',
'auxPHID' => 'phid?',
'corpus' => 'fulltext?',
'stemmedCorpus' => 'fulltext?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
'phid' => array(
'columns' => array('phid'),
),
'corpus' => array(
'columns' => array('corpus'),
'key_corpus' => array(
'columns' => array('corpus', 'stemmedCorpus'),
'type' => 'FULLTEXT',
),
),

View file

@ -502,6 +502,15 @@ abstract class PhabricatorApplicationTransactionEditor
return false;
}
$type = $xaction->getTransactionType();
$xtype = $this->getModularTransactionType($type);
if ($xtype) {
return $xtype->getTransactionHasEffect(
$object,
$xaction->getOldValue(),
$xaction->getNewValue());
}
return ($xaction->getOldValue() !== $xaction->getNewValue());
}

View file

@ -35,6 +35,10 @@ abstract class PhabricatorModularTransactionType
return;
}
public function getTransactionHasEffect($object, $old, $new) {
return ($old !== $new);
}
public function extractFilePHIDs($object, $value) {
return array();
}

View file

@ -635,7 +635,7 @@ final class PhabricatorDatabaseRef
$application_replicas = array();
$default_replicas = array();
foreach ($replicas as $replica) {
$master = $replica->getMaster();
$master = $replica->getMasterRef();
if ($master->isApplicationHost($application)) {
$application_replicas[] = $replica;

View file

@ -77,6 +77,17 @@ final class PhabricatorStorageManagementUpgradeWorkflow
$this->upgradeSchemata($apis, $apply_only, $no_quickstart, $init_only);
if ($apply_only || $init_only) {
echo tsprintf(
"%s\n",
pht('Declining to synchronize static tables.'));
} else {
echo tsprintf(
"%s\n",
pht('Synchronizing static tables...'));
$this->synchronizeSchemata();
}
if ($no_adjust || $init_only || $apply_only) {
$console->writeOut(
"%s\n",
@ -93,4 +104,46 @@ final class PhabricatorStorageManagementUpgradeWorkflow
return 0;
}
private function synchronizeSchemata() {
// Synchronize the InnoDB fulltext stopwords table from the stopwords file
// on disk.
$table = new PhabricatorSearchDocument();
$conn = $table->establishConnection('w');
$table_ref = PhabricatorSearchDocument::STOPWORDS_TABLE;
$stopwords_database = queryfx_all(
$conn,
'SELECT value FROM %T',
$table_ref);
$stopwords_database = ipull($stopwords_database, 'value', 'value');
$stopwords_path = phutil_get_library_root('phabricator');
$stopwords_path = $stopwords_path.'/../resources/sql/stopwords.txt';
$stopwords_file = Filesystem::readFile($stopwords_path);
$stopwords_file = phutil_split_lines($stopwords_file, false);
$stopwords_file = array_fuse($stopwords_file);
$rem_words = array_diff_key($stopwords_database, $stopwords_file);
if ($rem_words) {
queryfx(
$conn,
'DELETE FROM %T WHERE value IN (%Ls)',
$table_ref,
$rem_words);
}
$add_words = array_diff_key($stopwords_file, $stopwords_database);
if ($add_words) {
foreach ($add_words as $word) {
queryfx(
$conn,
'INSERT IGNORE INTO %T (value) VALUES (%s)',
$table_ref,
$word);
}
}
}
}

View file

@ -308,10 +308,11 @@ abstract class PhabricatorStorageManagementWorkflow
if ($phase == 'main') {
queryfx(
$conn,
'ALTER TABLE %T.%T COLLATE = %s',
'ALTER TABLE %T.%T COLLATE = %s, ENGINE = %s',
$adjust['database'],
$adjust['table'],
$adjust['collation']);
$adjust['collation'],
$adjust['engine']);
}
break;
case 'column':
@ -480,6 +481,7 @@ abstract class PhabricatorStorageManagementWorkflow
$issue_unique = PhabricatorConfigStorageSchema::ISSUE_UNIQUE;
$issue_longkey = PhabricatorConfigStorageSchema::ISSUE_LONGKEY;
$issue_auto = PhabricatorConfigStorageSchema::ISSUE_AUTOINCREMENT;
$issue_engine = PhabricatorConfigStorageSchema::ISSUE_ENGINE;
$adjustments = array();
$errors = array();
@ -543,6 +545,10 @@ abstract class PhabricatorStorageManagementWorkflow
$issues[] = $issue_collation;
}
if ($table->hasIssue($issue_engine)) {
$issues[] = $issue_engine;
}
if ($issues) {
$adjustments[] = array(
'kind' => 'table',
@ -550,6 +556,7 @@ abstract class PhabricatorStorageManagementWorkflow
'table' => $table_name,
'issues' => $issues,
'collation' => $expect_table->getCollation(),
'engine' => $expect_table->getEngine(),
);
}
@ -922,6 +929,7 @@ abstract class PhabricatorStorageManagementWorkflow
}
$applied_map = array();
$state_map = array();
foreach ($api_map as $ref_key => $api) {
$applied = $api->getAppliedPatches();
@ -939,6 +947,7 @@ abstract class PhabricatorStorageManagementWorkflow
}
$applied = array_fuse($applied);
$state_map[$ref_key] = $applied;
if ($apply_only) {
if (isset($applied[$apply_only])) {
@ -1090,7 +1099,7 @@ abstract class PhabricatorStorageManagementWorkflow
// If we're explicitly reapplying this patch, we don't need to
// mark it as applied.
if (!isset($applied_map[$ref_key][$key])) {
if (!isset($state_map[$ref_key][$key])) {
$api->markPatchApplied($key, ($t_end - $t_begin));
$applied_map[$ref_key][$key] = true;
}

View file

@ -2,11 +2,17 @@
final class PhabricatorLocalTimeTestCase extends PhabricatorTestCase {
protected function getPhabricatorTestCaseConfiguration() {
return array(
self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES => true,
);
}
public function testLocalTimeFormatting() {
$user = new PhabricatorUser();
$user = $this->generateNewTestUser();
$user->overrideTimezoneIdentifier('America/Los_Angeles');
$utc = new PhabricatorUser();
$utc = $this->generateNewTestUser();
$utc->overrideTimezoneIdentifier('UTC');
$this->assertEqual(

View file

@ -8,6 +8,7 @@ final class PhabricatorFileLinkView extends AphrontView {
private $fileViewable;
private $filePHID;
private $fileMonogram;
private $fileSize;
private $customClass;
public function setCustomClass($custom_class) {
@ -73,6 +74,19 @@ final class PhabricatorFileLinkView extends AphrontView {
return $this->fileName;
}
public function setFileSize($file_size) {
$this->fileSize = $file_size;
return $this;
}
private function getFileSize() {
return $this->fileSize;
}
private function getFileIcon() {
return FileTypeIcon::getFileIcon($this->getFileName());
}
public function getMetadata() {
return array(
'phid' => $this->getFilePHID(),
@ -81,6 +95,8 @@ final class PhabricatorFileLinkView extends AphrontView {
'dUri' => $this->getFileDownloadURI(),
'name' => $this->getFileName(),
'monogram' => $this->getFileMonogram(),
'icon' => $this->getFileIcon(),
'size' => $this->getFileSize(),
);
}
@ -98,7 +114,31 @@ final class PhabricatorFileLinkView extends AphrontView {
}
$icon = id(new PHUIIconView())
->setIcon('fa-file-text-o');
->setIcon($this->getFileIcon());
$info = phutil_tag(
'span',
array(
'class' => 'phabricator-remarkup-embed-layout-info',
),
$this->getFileSize());
$name = phutil_tag(
'span',
array(
'class' => 'phabricator-remarkup-embed-layout-name',
),
$this->getFileName());
$inner = phutil_tag(
'span',
array(
'class' => 'phabricator-remarkup-embed-layout-info-block',
),
array(
$name,
$info,
));
return javelin_tag(
'a',
@ -111,7 +151,7 @@ final class PhabricatorFileLinkView extends AphrontView {
),
array(
$icon,
$this->getFileName(),
$inner,
));
}
}

View file

@ -277,16 +277,17 @@ final class PhabricatorStandardPageView extends PhabricatorBarePageView
'action' => '#',
'method' => 'POST',
'class' => 'lightbox-download-form',
'sigil' => 'download',
'sigil' => 'download lightbox-download-submit',
'id' => 'lightbox-download-form',
),
phutil_tag(
'button',
'a',
array(
'class' => 'button grey has-icon',
'class' => 'lightbox-download phui-icon-circle hover-green',
'href' => '#',
),
array(
$icon,
pht('Download'),
)));
Javelin::initBehavior(
@ -511,6 +512,7 @@ final class PhabricatorStandardPageView extends PhabricatorBarePageView
'div',
array(
'class' => implode(' ', $classes),
'id' => 'main-page-frame',
),
array(
$main_page,

View file

@ -370,20 +370,41 @@ video.phabricator-media {
}
.phabricator-remarkup-embed-layout-link {
padding: 2px 0;
padding: 8px 8px 8px 32px;
border-radius: 3px;
margin: 0;
margin: 0 0 4px;
display: inline-block;
font-weight: bold;
color: {$anchor};
-webkit-font-smoothing: antialiased;
border: 1px solid {$lightblueborder};
border-radius: 3px;
color: #000;
min-width: 240px;
position: relative;
height: 22px;
line-height: 20px;
}
.phabricator-remarkup-embed-layout-link .phui-icon-view {
margin-right: 8px;
font-size: 20px;
position: absolute;
top: 8px;
left: 8px;
}
.phabricator-remarkup-embed-layout-info {
color: {$lightgreytext};
font-size: {$smallerfontsize};
font-weight: normal;
margin-left: 8px;
}
.phabricator-remarkup-embed-layout-link:hover {
border-color: {$violet};
text-decoration: none;
}
.phabricator-remarkup-embed-layout-link:hover,
.phabricator-remarkup-embed-layout-link:hover .phui-icon-view {
color: {$violet};
}

View file

@ -26,6 +26,11 @@
margin-left: 40px;
}
.device-phone .phui-comment-panel .phui-timeline-view
.phui-timeline-event-view {
margin-left: 16px;
}
.device-desktop .phui-comment-panel .phui-timeline-view .phui-timeline-image {
width: 30px;
height: 30px;
@ -73,6 +78,10 @@
border: none;
}
.phui-comment-panel .phui-timeline-badges {
display: none;
}
.lightbox-comment-form .phui-form-view {
padding-top: 0;
}

View file

@ -116,6 +116,7 @@ select {
border: 1px solid {$greyborder};
height: 28px;
padding: 0 24px 0 8px;
margin: 0;
min-width: 180px;
}

View file

@ -49,6 +49,8 @@ a.phui-icon-view:hover {
text-align: center;
display: inline-block;
cursor: pointer;
background: transparent;
padding: 0;
}
.phui-icon-circle.circle-medium {
@ -111,6 +113,16 @@ a.phui-icon-circle.hover-green:hover .phui-icon-view {
color: {$green};
}
a.phui-icon-circle.hover-red:hover {
text-decoration: none;
border-color: {$red};
cursor: pointer;
}
a.phui-icon-circle.hover-red:hover .phui-icon-view {
color: {$red};
}
/* - Icon in a Square ------------------------------------------------------- */
.phui-icon-view.phui-icon-square {

View file

@ -43,7 +43,6 @@
.lightbox-comment-frame {
position: absolute;
top: -19999px;
bottom: 0;
right: 0;
opacity: 0;
transition: all 0.3s;
@ -56,6 +55,13 @@
display: block;
height: 120px;
width: 320px;
color: {$darkbluetext};
}
.lightbox-attachment .lightbox-icon-frame:hover,
.lightbox-attachment .lightbox-icon-frame:hover .phui-icon-view {
color: {$anchor};
text-decoration: none;
}
.lightbox-attachment.comment-panel-open .lightbox-icon-frame {
@ -78,6 +84,11 @@
opacity: 1;
}
.device-phone .comment-panel-open .lightbox-comment-frame {
width: auto;
left: 0;
}
.jx-mask + .lightbox-attachment {
background: {$lightgreybackground};
}
@ -101,18 +112,26 @@
color: {$greytext};
}
.device-phone .lightbox-attachment .lightbox-status {
padding: 0 12px;
}
.lightbox-attachment .lightbox-status .lightbox-download {
float: right;
}
.lightbox-attachment .lightbox-status a {
.lightbox-attachment .lightbox-status-txt a {
color: #000;
margin-right: 4px;
margin-right: 12px;
font-size: {$biggerfontsize};
}
.lightbox-download button.has-icon {
padding-left: 28px;
.lightbox-attachment .lightbox-status .phui-icon-view {
height: 18px;
width: 24px;
font-size: 14px;
line-height: 23px;
display: block;
}
.lightbox-attachment .lightbox-status .lightbox-download
@ -120,20 +139,17 @@
display: inline;
}
.lightbox-attachment .lightbox-comment {
.lightbox-attachment a.lightbox-download,
.lightbox-attachment a.lightbox-comment,
.lightbox-attachment a.lightbox-close {
float: right;
margin: 9px 0 0 8px;
padding-left: 28px;
}
.lightbox-attachment.comment-panel-open .lightbox-comment,
.lightbox-attachment.comment-panel-open .lightbox-comment.phui-icon-circle,
.lightbox-attachment.comment-panel-open .lightbox-comment .phui-icon-view {
color: {$sky};
}
.lightbox-attachment .lightbox-close {
float: right;
margin: 9px 0 0 8px;
border-color: {$sky};
}
.lightbox-attachment .lightbox-left {

View file

@ -2,6 +2,10 @@
* @provides phui-workboard-color-css
*/
.phui-workboard-color .phabricator-nav-content .phui-workboard-view-shadow {
background-color: transparent;
}
.phui-workboard-color .phui-crumbs-view {
background-color: rgba({$alphagrey},.15);
border: none;
@ -26,12 +30,36 @@
background-color: rgba({$alphawhite},.6);
}
body.phui-workboard-color .phui-profile-menu .phabricator-side-menu {
background-color: rgba({$alphagrey},.3);
}
body.phui-workboard-color .phabricator-side-menu .phui-profile-menu-footer-1 {
background-color: transparent;
}
.phui-workboard-color .phui-profile-menu .phabricator-side-menu {
box-shadow: none;
}
.phui-workboard-color-preview {
width: 50px;
height: 50px;
font-size: 34px;
}
.phui-workboard-color .phui-profile-menu .phabricator-side-menu
.phui-list-item-href {
color: rgba({$alphawhite},.8);
}
.phui-workboard-color .phui-profile-menu .phabricator-side-menu
.phui-list-item-icon,
.phui-workboard-color .phui-profile-menu .phabricator-side-menu
.phui-list-item-href .phui-list-item-icon {
color: rgba({$alphawhite},.8);
}
/* Gradients */
.phui-workboard-gradient-red {

View file

@ -31,6 +31,15 @@
background: {$lightbluetext};
}
.device-desktop .project-board-wrapper .phui-workboard-view-shadow {
left: {$menu.profile.width};
}
.device-desktop .phui-profile-menu-collapsed .project-board-wrapper
.phui-workboard-view-shadow {
left: {$menu.profile.width.collapsed};
}
!print .project-board-wrapper .phui-workboard-view-shadow {
position: static;
}
@ -65,7 +74,8 @@
display: none;
}
.device-desktop .phui-workboard-fullscreen .phui-workboard-view-shadow {
.device-desktop .phui-workboard-fullscreen .phui-profile-menu
.phui-workboard-view-shadow {
top: 35px;
left: 0;
}
@ -74,6 +84,11 @@
max-height: calc(100vh - 120px);
}
.device-desktop .phui-workboard-fullscreen .phui-profile-menu
.phabricator-nav-local {
display: none;
}
.device .phui-workboard-expand-icon {
display: none;
}

View file

@ -468,6 +468,7 @@ JX.install('ConpherenceThreadManager', {
}
}));
this.syncWorkflow(workflow, 'finally');
textarea.value = '';
this._willSendMessageCallback();
},

View file

@ -50,7 +50,8 @@ JX.behavior('lightbox-attachments', function (config) {
e.kill();
var links = JX.DOM.scry(document, 'a', 'lightboxable');
var mainFrame = JX.$('main-page-frame');
var links = JX.DOM.scry(mainFrame, 'a', 'lightboxable');
var phids = {};
var data;
for (var i = 0; i < links.length; i++) {
@ -99,7 +100,7 @@ JX.behavior('lightbox-attachments', function (config) {
);
} else {
var imgIcon = new JX.PHUIXIconView()
.setIcon('fa-file-text-o phui-lightbox-file-icon')
.setIcon(target_data.icon + ' phui-lightbox-file-icon')
.getNode();
var nameElement =
JX.$N('div',
@ -109,9 +110,11 @@ JX.behavior('lightbox-attachments', function (config) {
target_data.name
);
img =
JX.$N('div',
JX.$N('a',
{
className : 'lightbox-icon-frame',
sigil : 'lightbox-download-submit',
href : '#',
},
[ imgIcon, nameElement ]
);
@ -121,6 +124,7 @@ JX.behavior('lightbox-attachments', function (config) {
JX.$N('div',
{
className : 'lightbox-image-frame',
sigil : 'lightbox-image-frame',
},
img
);
@ -153,7 +157,7 @@ JX.behavior('lightbox-attachments', function (config) {
},
[
m_url,
' Image ' + current + ' of ' + total + '.'
current + ' / ' + total
]
);
@ -165,24 +169,27 @@ JX.behavior('lightbox-attachments', function (config) {
);
var commentIcon = new JX.PHUIXIconView()
.setIcon('fa-comment-o')
.setIcon('fa-comments')
.getNode();
var commentButton =
JX.$N('a',
{
className : 'lightbox-comment button grey has-icon',
className : 'lightbox-comment phui-icon-circle hover-sky',
href : '#',
sigil : 'lightbox-comment'
},
[commentIcon, 'Comment']
commentIcon
);
var closeIcon = new JX.PHUIXIconView()
.setIcon('fa-times')
.getNode();
var closeButton =
JX.$N('a',
{
className : 'lightbox-close button grey',
className : 'lightbox-close phui-icon-circle hover-red',
href : '#'
},
'Close');
closeIcon);
var statusHTML =
JX.$N('div',
{
@ -311,7 +318,9 @@ JX.behavior('lightbox-attachments', function (config) {
el.click();
}
JX.Stratcom.listen(
// Only look for lightboxable inside the main page, not other lightboxes.
JX.DOM.listen(
JX.$('main-page-frame'),
'click',
['lightboxable'],
loadLightBox);
@ -324,12 +333,12 @@ JX.behavior('lightbox-attachments', function (config) {
// When the user clicks the background, close the lightbox.
JX.Stratcom.listen(
'click',
'lightbox-attachment',
'lightbox-image-frame',
function (e) {
if (!lightbox) {
return;
}
if (e.getTarget() != e.getNode('lightbox-attachment')) {
if (e.getTarget() != e.getNode('lightbox-image-frame')) {
// Don't close if they clicked some other element, like the image
// itself or the next/previous arrows.
return;
@ -356,4 +365,15 @@ JX.behavior('lightbox-attachments', function (config) {
'lightbox-comment-form',
_sendMessage);
var _startDownload = function(e) {
e.kill();
var form = JX.$('lightbox-download-form');
form.submit();
};
JX.Stratcom.listen(
'click',
'lightbox-download-submit',
_startDownload);
});