1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-15 18:10:53 +01:00

(stable) Promote 2017 Week 11

This commit is contained in:
epriestley 2017-03-17 17:20:29 -07:00
commit da80ed415f
38 changed files with 844 additions and 155 deletions

View file

@ -656,6 +656,10 @@ class PHPMailerLite {
$addr_str .= implode(', ', $addresses);
$addr_str .= $this->LE;
// NOTE: This is a narrow hack to fix an issue with 1000+ characters of
// recipients, described in T12372.
$addr_str = wordwrap($addr_str, 75, "\n ");
return $addr_str;
}

View file

@ -7,9 +7,9 @@
*/
return array(
'names' => array(
'conpherence.pkg.css' => '6875302f',
'conpherence.pkg.css' => '32f2c040',
'conpherence.pkg.js' => '6249a1cf',
'core.pkg.css' => '35645dec',
'core.pkg.css' => '491d7018',
'core.pkg.js' => '1fa7c0c5',
'darkconsole.pkg.js' => 'e7393ebb',
'differential.pkg.css' => '90b30783',
@ -21,7 +21,7 @@ return array(
'maniphest.pkg.js' => '5ab2753f',
'rsrc/css/aphront/aphront-bars.css' => '231ac33c',
'rsrc/css/aphront/dark-console.css' => 'f54bf286',
'rsrc/css/aphront/dialog-view.css' => '5e5aa60b',
'rsrc/css/aphront/dialog-view.css' => '685c7e2d',
'rsrc/css/aphront/list-filter-view.css' => '5d6f0526',
'rsrc/css/aphront/multi-column.css' => '84cc6640',
'rsrc/css/aphront/notification.css' => '3f6c89c9',
@ -48,7 +48,7 @@ return array(
'rsrc/css/application/conpherence/durable-column.css' => '292c71f0',
'rsrc/css/application/conpherence/header-pane.css' => 'db93ebc6',
'rsrc/css/application/conpherence/menu.css' => '3d8e5c9c',
'rsrc/css/application/conpherence/message-pane.css' => 'b085d40d',
'rsrc/css/application/conpherence/message-pane.css' => 'd1fc13e1',
'rsrc/css/application/conpherence/notification.css' => '965db05b',
'rsrc/css/application/conpherence/participant-pane.css' => '604a8b02',
'rsrc/css/application/conpherence/transaction.css' => '85129c68',
@ -146,7 +146,7 @@ return array(
'rsrc/css/phui/phui-document.css' => 'c32e8dec',
'rsrc/css/phui/phui-feed-story.css' => '44a9c8e9',
'rsrc/css/phui/phui-fontkit.css' => 'b78a0059',
'rsrc/css/phui/phui-form-view.css' => 'adca31ce',
'rsrc/css/phui/phui-form-view.css' => 'cf198e10',
'rsrc/css/phui/phui-form.css' => 'b62c01d8',
'rsrc/css/phui/phui-head-thing.css' => 'fd311e5f',
'rsrc/css/phui/phui-header-view.css' => 'fef6a54e',
@ -158,7 +158,7 @@ return array(
'rsrc/css/phui/phui-info-view.css' => 'ec92802a',
'rsrc/css/phui/phui-invisible-character-view.css' => '6993d9f0',
'rsrc/css/phui/phui-lightbox.css' => '0a035e40',
'rsrc/css/phui/phui-list.css' => '9da2aa00',
'rsrc/css/phui/phui-list.css' => 'a3ec3cf1',
'rsrc/css/phui/phui-object-box.css' => '8b289e3d',
'rsrc/css/phui/phui-pager.css' => '77d8a794',
'rsrc/css/phui/phui-pinboard-view.css' => '2495140e',
@ -548,7 +548,7 @@ return array(
'almanac-css' => 'dbb9b3af',
'aphront-bars' => '231ac33c',
'aphront-dark-console-css' => 'f54bf286',
'aphront-dialog-view-css' => '5e5aa60b',
'aphront-dialog-view-css' => '685c7e2d',
'aphront-list-filter-view-css' => '5d6f0526',
'aphront-multi-column-view-css' => '84cc6640',
'aphront-panel-view-css' => '8427b78d',
@ -566,7 +566,7 @@ return array(
'conpherence-durable-column-view' => '292c71f0',
'conpherence-header-pane-css' => 'db93ebc6',
'conpherence-menu-css' => '3d8e5c9c',
'conpherence-message-pane-css' => 'b085d40d',
'conpherence-message-pane-css' => 'd1fc13e1',
'conpherence-notification-css' => '965db05b',
'conpherence-participant-pane-css' => '604a8b02',
'conpherence-thread-manager' => 'c8b5ee6f',
@ -859,7 +859,7 @@ return array(
'phui-font-icon-base-css' => '870a7360',
'phui-fontkit-css' => 'b78a0059',
'phui-form-css' => 'b62c01d8',
'phui-form-view-css' => 'adca31ce',
'phui-form-view-css' => 'cf198e10',
'phui-head-thing-view-css' => 'fd311e5f',
'phui-header-view-css' => 'fef6a54e',
'phui-hovercard' => '1bd28176',
@ -872,7 +872,7 @@ return array(
'phui-inline-comment-view-css' => 'be663c95',
'phui-invisible-character-view-css' => '6993d9f0',
'phui-lightbox-css' => '0a035e40',
'phui-list-view-css' => '9da2aa00',
'phui-list-view-css' => 'a3ec3cf1',
'phui-object-box-css' => '8b289e3d',
'phui-oi-big-ui-css' => '19f9369b',
'phui-oi-color-css' => 'cd2b9b77',

View file

@ -0,0 +1,9 @@
CREATE TABLE {$NAMESPACE}_differential.differential_reviewer (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
revisionPHID VARBINARY(64) NOT NULL,
reviewerPHID VARBINARY(64) NOT NULL,
reviewerStatus VARCHAR(64) NOT NULL COLLATE {$COLLATE_TEXT},
dateCreated INT UNSIGNED NOT NULL,
dateModified INT UNSIGNED NOT NULL,
UNIQUE KEY `key_revision` (revisionPHID, reviewerPHID)
) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};

View file

@ -491,6 +491,7 @@ phutil_register_library_map(array(
'DifferentialRevertPlanCommitMessageField' => 'applications/differential/field/DifferentialRevertPlanCommitMessageField.php',
'DifferentialRevertPlanField' => 'applications/differential/customfield/DifferentialRevertPlanField.php',
'DifferentialReviewedByCommitMessageField' => 'applications/differential/field/DifferentialReviewedByCommitMessageField.php',
'DifferentialReviewer' => 'applications/differential/storage/DifferentialReviewer.php',
'DifferentialReviewerDatasource' => 'applications/differential/typeahead/DifferentialReviewerDatasource.php',
'DifferentialReviewerForRevisionEdgeType' => 'applications/differential/edge/DifferentialReviewerForRevisionEdgeType.php',
'DifferentialReviewerProxy' => 'applications/differential/storage/DifferentialReviewerProxy.php',
@ -2495,6 +2496,7 @@ phutil_register_library_map(array(
'PhabricatorDashboardEditController' => 'applications/dashboard/controller/PhabricatorDashboardEditController.php',
'PhabricatorDashboardIconSet' => 'applications/dashboard/icon/PhabricatorDashboardIconSet.php',
'PhabricatorDashboardInstall' => 'applications/dashboard/storage/PhabricatorDashboardInstall.php',
'PhabricatorDashboardInstallController' => 'applications/dashboard/controller/PhabricatorDashboardInstallController.php',
'PhabricatorDashboardLayoutConfig' => 'applications/dashboard/layoutconfig/PhabricatorDashboardLayoutConfig.php',
'PhabricatorDashboardListController' => 'applications/dashboard/controller/PhabricatorDashboardListController.php',
'PhabricatorDashboardManageController' => 'applications/dashboard/controller/PhabricatorDashboardManageController.php',
@ -3761,6 +3763,7 @@ phutil_register_library_map(array(
'PhabricatorSearchBaseController' => 'applications/search/controller/PhabricatorSearchBaseController.php',
'PhabricatorSearchCheckboxesField' => 'applications/search/field/PhabricatorSearchCheckboxesField.php',
'PhabricatorSearchConfigOptions' => 'applications/search/config/PhabricatorSearchConfigOptions.php',
'PhabricatorSearchConstraintException' => 'applications/search/exception/PhabricatorSearchConstraintException.php',
'PhabricatorSearchController' => 'applications/search/controller/PhabricatorSearchController.php',
'PhabricatorSearchCustomFieldProxyField' => 'applications/search/field/PhabricatorSearchCustomFieldProxyField.php',
'PhabricatorSearchDAO' => 'applications/search/storage/PhabricatorSearchDAO.php',
@ -4064,6 +4067,7 @@ phutil_register_library_map(array(
'PhabricatorUnknownContentSource' => 'infrastructure/contentsource/PhabricatorUnknownContentSource.php',
'PhabricatorUnsubscribedFromObjectEdgeType' => 'applications/transactions/edges/PhabricatorUnsubscribedFromObjectEdgeType.php',
'PhabricatorUser' => 'applications/people/storage/PhabricatorUser.php',
'PhabricatorUserBadgesCacheType' => 'applications/people/cache/PhabricatorUserBadgesCacheType.php',
'PhabricatorUserBlurbField' => 'applications/people/customfield/PhabricatorUserBlurbField.php',
'PhabricatorUserCache' => 'applications/people/storage/PhabricatorUserCache.php',
'PhabricatorUserCacheType' => 'applications/people/cache/PhabricatorUserCacheType.php',
@ -5247,6 +5251,7 @@ phutil_register_library_map(array(
'DifferentialRevertPlanCommitMessageField' => 'DifferentialCommitMessageCustomField',
'DifferentialRevertPlanField' => 'DifferentialStoredCustomField',
'DifferentialReviewedByCommitMessageField' => 'DifferentialCommitMessageField',
'DifferentialReviewer' => 'DifferentialDAO',
'DifferentialReviewerDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'DifferentialReviewerForRevisionEdgeType' => 'PhabricatorEdgeType',
'DifferentialReviewerProxy' => 'Phobject',
@ -7560,6 +7565,7 @@ phutil_register_library_map(array(
'PhabricatorDashboardEditController' => 'PhabricatorDashboardController',
'PhabricatorDashboardIconSet' => 'PhabricatorIconSet',
'PhabricatorDashboardInstall' => 'PhabricatorDashboardDAO',
'PhabricatorDashboardInstallController' => 'PhabricatorDashboardController',
'PhabricatorDashboardLayoutConfig' => 'Phobject',
'PhabricatorDashboardListController' => 'PhabricatorDashboardController',
'PhabricatorDashboardManageController' => 'PhabricatorDashboardProfileController',
@ -9068,6 +9074,7 @@ phutil_register_library_map(array(
'PhabricatorSearchBaseController' => 'PhabricatorController',
'PhabricatorSearchCheckboxesField' => 'PhabricatorSearchField',
'PhabricatorSearchConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorSearchConstraintException' => 'Exception',
'PhabricatorSearchController' => 'PhabricatorSearchBaseController',
'PhabricatorSearchCustomFieldProxyField' => 'PhabricatorSearchField',
'PhabricatorSearchDAO' => 'PhabricatorLiskDAO',
@ -9409,6 +9416,7 @@ phutil_register_library_map(array(
'PhabricatorFulltextInterface',
'PhabricatorConduitResultInterface',
),
'PhabricatorUserBadgesCacheType' => 'PhabricatorUserCacheType',
'PhabricatorUserBlurbField' => 'PhabricatorUserCustomField',
'PhabricatorUserCache' => 'PhabricatorUserDAO',
'PhabricatorUserCacheType' => 'Phobject',

View file

@ -306,6 +306,18 @@ final class PhabricatorAuditEditor
$field_key = DifferentialAuditorsCommitMessageField::FIELDKEY;
$phids = idx($result, $field_key, null);
if (!$phids) {
return array();
}
// If a commit lists its author as an auditor, just pretend it does not.
foreach ($phids as $key => $phid) {
if ($phid == $commit->getAuthorPHID()) {
unset($phids[$key]);
}
}
if (!$phids) {
return array();
}

View file

@ -118,4 +118,45 @@ final class PhabricatorBadgesEditor
return pht('[Badge]');
}
protected function applyFinalEffects(
PhabricatorLiskDAO $object,
array $xactions) {
$badge_phid = $object->getPHID();
$user_phids = array();
$clear_everything = false;
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorBadgesBadgeAwardTransaction::TRANSACTIONTYPE:
case PhabricatorBadgesBadgeRevokeTransaction::TRANSACTIONTYPE:
foreach ($xaction->getNewValue() as $user_phid) {
$user_phids[] = $user_phid;
}
break;
default:
$clear_everything = true;
break;
}
}
if ($clear_everything) {
$awards = id(new PhabricatorBadgesAwardQuery())
->setViewer($this->getActor())
->withBadgePHIDs(array($badge_phid))
->execute();
foreach ($awards as $award) {
$user_phids[] = $award->getRecipientPHID();
}
}
if ($user_phids) {
PhabricatorUserCache::clearCaches(
PhabricatorUserBadgesCacheType::KEY_BADGES,
$user_phids);
}
return $xactions;
}
}

View file

@ -6,7 +6,7 @@ final class PhabricatorBadgesAwardQuery
private $badgePHIDs;
private $recipientPHIDs;
private $awarderPHIDs;
private $badgeStatuses = null;
protected function willFilterPage(array $awards) {
$badge_phids = array();
@ -22,6 +22,11 @@ final class PhabricatorBadgesAwardQuery
$badges = mpull($badges, null, 'getPHID');
foreach ($awards as $key => $award) {
$award_badge = idx($badges, $award->getBadgePHID());
if (!$award_badge) {
unset($awards[$key]);
$this->didRejectResult($award);
continue;
}
$award->attachBadge($award_badge);
}
@ -43,6 +48,15 @@ final class PhabricatorBadgesAwardQuery
return $this;
}
public function withBadgeStatuses(array $statuses) {
$this->badgeStatuses = $statuses;
return $this;
}
private function shouldJoinBadge() {
return (bool)$this->badgeStatuses;
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
@ -51,33 +65,59 @@ final class PhabricatorBadgesAwardQuery
return new PhabricatorBadgesAward();
}
protected function getPrimaryTableAlias() {
return 'badges_award';
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->badgePHIDs !== null) {
$where[] = qsprintf(
$conn,
'badgePHID IN (%Ls)',
'badges_award.badgePHID IN (%Ls)',
$this->badgePHIDs);
}
if ($this->recipientPHIDs !== null) {
$where[] = qsprintf(
$conn,
'recipientPHID IN (%Ls)',
'badges_award.recipientPHID IN (%Ls)',
$this->recipientPHIDs);
}
if ($this->awarderPHIDs !== null) {
$where[] = qsprintf(
$conn,
'awarderPHID IN (%Ls)',
'badges_award.awarderPHID IN (%Ls)',
$this->awarderPHIDs);
}
if ($this->badgeStatuses !== null) {
$where[] = qsprintf(
$conn,
'badges_badge.status IN (%Ls)',
$this->badgeStatuses);
}
return $where;
}
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
$join = parent::buildJoinClauseParts($conn);
$badges = new PhabricatorBadgesBadge();
if ($this->shouldJoinBadge()) {
$join[] = qsprintf(
$conn,
'JOIN %T badges_badge ON badges_award.badgePHID = badges_badge.phid',
$badges->getTableName());
}
return $join;
}
public function getQueryApplicationClass() {
return 'PhabricatorBadgesApplication';
}

View file

@ -36,7 +36,7 @@ final class PhabricatorBadgesRecipientsListView extends AphrontView {
$award_button = id(new PHUIButtonView())
->setTag('a')
->setIcon('fa-plus')
->setText(pht('Add Recipents'))
->setText(pht('Add Recipients'))
->setWorkflow(true)
->setDisabled(!$can_edit)
->setHref('/badges/recipients/'.$badge->getID().'/add/');

View file

@ -158,6 +158,10 @@ final class PhabricatorConfigManagementSetWorkflow
$config_type = 'database';
$config_entry = PhabricatorConfigEntry::loadConfigEntry($key);
$config_entry->setValue($value);
// If the entry has been deleted, resurrect it.
$config_entry->setIsDeleted(0);
$config_entry->save();
} else {
$config_type = 'local';

View file

@ -66,6 +66,21 @@ EOTEXT
,
PhabricatorEnv::getDoclink('Configuring Encryption')));
$require_mfa_description = $this->deformat(pht(<<<EOTEXT
By default, Phabricator allows users to add multi-factor authentication to
their accounts, but does not require it. By enabling this option, you can
force all users to add at least one authentication factor before they can use
their accounts.
Administrators can query a list of users who do not have MFA configured in
{nav People}:
- **[[ %s | %s ]]**
EOTEXT
,
'/people/?mfa=false',
pht('List of Users Without MFA')));
return array(
$this->newOption('security.alternate-file-domain', 'string', null)
->setLocked(true)
@ -132,13 +147,7 @@ EOTEXT
->setLocked(true)
->setSummary(
pht('Require all users to configure multi-factor authentication.'))
->setDescription(
pht(
'By default, Phabricator allows users to add multi-factor '.
'authentication to their accounts, but does not require it. '.
'By enabling this option, you can force all users to add '.
'at least one authentication factor before they can use their '.
'accounts.'))
->setDescription($require_mfa_description)
->setBoolOptions(
array(
pht('Multi-Factor Required'),

View file

@ -30,6 +30,7 @@ final class PhabricatorDashboardApplication extends PhabricatorApplication {
'arrange/(?P<id>\d+)/' => 'PhabricatorDashboardArrangeController',
'create/' => 'PhabricatorDashboardEditController',
'edit/(?:(?P<id>\d+)/)?' => 'PhabricatorDashboardEditController',
'install/(?:(?P<id>\d+)/)?' => 'PhabricatorDashboardInstallController',
'addpanel/(?P<id>\d+)/' => 'PhabricatorDashboardAddPanelController',
'movepanel/(?P<id>\d+)/' => 'PhabricatorDashboardMovePanelController',
'removepanel/(?P<id>\d+)/'

View file

@ -51,6 +51,14 @@ final class PhabricatorDashboardArrangeController
->addClass('dashboard-preview-box')
->appendChild($rendered_dashboard);
$install_button = id(new PHUIButtonView())
->setTag('a')
->setText('Install Dashboard')
->setIcon('fa-plus')
->setWorkflow(true)
->setHref($this->getApplicationURI("/install/{$id}/"));
$header->addActionLink($install_button);
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setFooter(array(

View file

@ -0,0 +1,141 @@
<?php
final class PhabricatorDashboardInstallController
extends PhabricatorDashboardController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$id = $request->getURIData('id');
$dashboard = id(new PhabricatorDashboardQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
if (!$dashboard) {
return new Aphront404Response();
}
$cancel_uri = $this->getApplicationURI(
'view/'.$dashboard->getID().'/');
$home_app = new PhabricatorHomeApplication();
$options = array();
$options['home'] = array(
'personal' =>
array(
'capability' => PhabricatorPolicyCapability::CAN_VIEW,
'application' => $home_app,
'name' => pht('Personal Dashboard'),
'value' => 'personal',
'description' => pht('Places this dashboard as a menu item on home '.
'as a personal menu item. It will only be on your personal '.
'home.'),
),
'global' =>
array(
'capability' => PhabricatorPolicyCapability::CAN_EDIT,
'application' => $home_app,
'name' => pht('Global Dashboard'),
'value' => 'global',
'description' => pht('Places this dashboard as a menu item on home '.
'as a global menu item. It will be available to all users.'),
),
);
$errors = array();
$v_name = null;
if ($request->isFormPost()) {
$menuitem = new PhabricatorDashboardProfileMenuItem();
$dashboard_phid = $dashboard->getPHID();
$home = new PhabricatorHomeApplication();
$v_name = $request->getStr('name');
$v_home = $request->getStr('home');
if ($v_home) {
$application = $options['home'][$v_home]['application'];
$capability = $options['home'][$v_home]['capability'];
$can_edit_home = PhabricatorPolicyFilter::hasCapability(
$viewer,
$application,
$capability);
if (!$can_edit_home) {
$errors[] = pht(
'You do not have permission to install a dashboard on home.');
}
} else {
$errors[] = pht(
'You must select a destination to install this dashboard.');
}
$v_phid = $viewer->getPHID();
if ($v_home == 'global') {
$v_phid = null;
}
if (!$errors) {
$install = PhabricatorProfileMenuItemConfiguration::initializeNewItem(
$home,
$menuitem,
$v_phid);
$install->setMenuItemProperty('dashboardPHID', $dashboard_phid);
$install->setMenuItemProperty('name', $v_name);
$install->setMenuItemOrder(1);
$xactions = array();
$editor = id(new PhabricatorProfileMenuEditor())
->setActor($viewer)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->setContentSourceFromRequest($request);
$editor->applyTransactions($install, $xactions);
$view_uri = '/home/menu/view/'.$install->getID().'/';
return id(new AphrontRedirectResponse())->setURI($view_uri);
}
}
$form = id(new AphrontFormView())
->setUser($viewer)
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Menu Label'))
->setName('name')
->setValue($v_name));
$radio = id(new AphrontFormRadioButtonControl())
->setLabel(pht('Home Menu'))
->setName('home');
foreach ($options['home'] as $type => $option) {
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$option['application'],
$option['capability']);
if ($can_edit) {
$radio->addButton(
$option['value'],
$option['name'],
$option['description']);
}
}
$form->appendChild($radio);
return $this->newDialog()
->setTitle(pht('Install Dashboard'))
->setErrors($errors)
->setWidth(AphrontDialogView::WIDTH_FORM)
->appendChild($form->buildLayoutView())
->addCancelButton($cancel_uri)
->addSubmitButton(pht('Install Dashboard'));
}
}

View file

@ -43,6 +43,14 @@ final class PhabricatorDashboardViewController
$navigation = $this->buildSideNavView('view');
$header = $this->buildHeaderView();
$install_button = id(new PHUIButtonView())
->setTag('a')
->setText('Install Dashboard')
->setIcon('fa-plus')
->setWorkflow(true)
->setHref($this->getApplicationURI("/install/{$id}/"));
$header->addActionLink($install_button);
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setFooter(array(

View file

@ -50,24 +50,10 @@ final class DifferentialRevisionIDCommitMessageField
$uri = new PhutilURI($uri_string);
$path = $uri->getPath();
$matches = null;
if (preg_match('#^/D(\d+)$#', $path, $matches)) {
$id = (int)$matches[1];
$prod_uri = new PhutilURI(PhabricatorEnv::getProductionURI('/D'.$id));
// Make sure the URI is the same as our URI. Basically, we want to ignore
// commits from other Phabricator installs.
if ($uri->getDomain() == $prod_uri->getDomain()) {
return $id;
}
$allowed_uris = PhabricatorEnv::getAllowedURIs('/D'.$id);
foreach ($allowed_uris as $allowed_uri) {
if ($uri_string == $allowed_uri) {
return $id;
}
if (PhabricatorEnv::isSelfURI($uri_string)) {
$matches = null;
if (preg_match('#^/D(\d+)$#', $path, $matches)) {
return (int)$matches[1];
}
}

View file

@ -13,6 +13,7 @@ final class DifferentialCommitMessageFieldTestCase
"D123\nSome-Custom-Field: The End" => 123,
"{$base_uri}D123" => 123,
"{$base_uri}D123\nSome-Custom-Field: The End" => 123,
'https://www.other.com/D123' => null,
);
$env = PhabricatorEnv::beginScopedEnv();

View file

@ -0,0 +1,24 @@
<?php
final class DifferentialReviewer
extends DifferentialDAO {
protected $revisionPHID;
protected $reviewerPHID;
protected $reviewerStatus;
protected function getConfiguration() {
return array(
self::CONFIG_COLUMN_SCHEMA => array(
'reviewerStatus' => 'text64',
),
self::CONFIG_KEY_SCHEMA => array(
'key_revision' => array(
'columns' => array('revisionPHID', 'reviewerPHID'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
}

View file

@ -116,6 +116,9 @@ abstract class DifferentialRevisionReviewTransaction
);
}
// This is currently double-writing: to the old (edge) store and the new
// (reviewer) store. Do the old edge write first.
$src_phid = $revision->getPHID();
$edge_type = DifferentialRevisionHasReviewerEdgeType::EDGECONST;
@ -131,6 +134,42 @@ abstract class DifferentialRevisionReviewTransaction
}
$editor->save();
// Now, do the new write.
if ($map) {
$table = new DifferentialReviewer();
$reviewers = $table->loadAllWhere(
'revisionPHID = %s AND reviewerPHID IN (%Ls)',
$src_phid,
array_keys($map));
$reviewers = mpull($reviewers, null, 'getReviewerPHID');
foreach ($map as $dst_phid => $edge_data) {
$reviewer = idx($reviewers, $dst_phid);
if (!$reviewer) {
$reviewer = id(new DifferentialReviewer())
->setRevisionPHID($src_phid)
->setReviewerPHID($dst_phid);
}
$reviewer->setReviewerStatus($status);
if ($status == DifferentialReviewerStatus::STATUS_RESIGNED) {
if ($reviewer->getID()) {
$reviewer->delete();
}
} else {
try {
$reviewer->save();
} catch (AphrontDuplicateKeyQueryException $ex) {
// At least for now, just ignore it if we lost a race.
}
}
}
}
}
}

View file

@ -106,6 +106,9 @@ final class DifferentialRevisionReviewersTransaction
public function applyExternalEffects($object, $value) {
$src_phid = $object->getPHID();
// This is currently double-writing: to the old (edge) store and the new
// (reviewer) store. Do the old edge write first.
$old = $this->generateOldValue($object);
$new = $value;
$edge_type = DifferentialRevisionHasReviewerEdgeType::EDGECONST;
@ -138,6 +141,51 @@ final class DifferentialRevisionReviewersTransaction
}
$editor->save();
// Now, do the new write.
$table = new DifferentialReviewer();
$table_name = $table->getTableName();
$conn = $table->establishConnection('w');
if ($rem) {
queryfx(
$conn,
'DELETE FROM %T WHERE revisionPHID = %s AND reviewerPHID IN (%Ls)',
$table_name,
$src_phid,
array_keys($rem));
}
if ($new) {
$reviewers = $table->loadAllWhere(
'revisionPHID = %s AND reviewerPHID IN (%Ls)',
$src_phid,
array_keys($new));
$reviewers = mpull($reviewers, null, 'getReviewerPHID');
foreach ($new as $dst_phid => $status) {
$old_status = idx($old, $dst_phid);
if ($old_status === $status) {
continue;
}
$reviewer = idx($reviewers, $dst_phid);
if (!$reviewer) {
$reviewer = id(new DifferentialReviewer())
->setRevisionPHID($src_phid)
->setReviewerPHID($dst_phid);
}
$reviewer->setReviewerStatus($status);
try {
$reviewer->save();
} catch (AphrontDuplicateKeyQueryException $ex) {
// At least for now, just ignore it if we lost a race.
}
}
}
}
public function getTitle() {

View file

@ -0,0 +1,61 @@
<?php
final class PhabricatorUserBadgesCacheType
extends PhabricatorUserCacheType {
const CACHETYPE = 'badges.award';
const KEY_BADGES = 'user.badge.award.v1';
const BADGE_COUNT = 2;
public function getAutoloadKeys() {
return array(
self::KEY_BADGES,
);
}
public function canManageKey($key) {
return ($key === self::KEY_BADGES);
}
public function getValueFromStorage($value) {
return phutil_json_decode($value);
}
public function newValueForUsers($key, array $users) {
if (!$users) {
return array();
}
$user_phids = mpull($users, 'getPHID');
$results = array();
foreach ($user_phids as $user_phid) {
$awards = id(new PhabricatorBadgesAwardQuery())
->setViewer($this->getViewer())
->withRecipientPHIDs(array($user_phid))
->withBadgeStatuses(array(PhabricatorBadgesBadge::STATUS_ACTIVE))
->setLimit(self::BADGE_COUNT)
->execute();
$award_data = array();
if ($awards) {
foreach ($awards as $award) {
$badge = $award->getBadge();
$award_data[] = array(
'icon' => $badge->getIcon(),
'name' => $badge->getName(),
'quality' => $badge->getQuality(),
'id' => $badge->getID(),
);
}
}
$results[$user_phid] = phutil_json_encode($award_data);
}
return $results;
}
}

View file

@ -84,15 +84,14 @@ final class PhabricatorPeopleProfileBadgesController
$awards = id(new PhabricatorBadgesAwardQuery())
->setViewer($viewer)
->withRecipientPHIDs(array($user->getPHID()))
->withBadgeStatuses(array(PhabricatorBadgesBadge::STATUS_ACTIVE))
->execute();
$awards = mpull($awards, null, 'getBadgePHID');
$badges = array();
foreach ($awards as $award) {
$badge = $award->getBadge();
if ($badge->getStatus() == PhabricatorBadgesBadge::STATUS_ACTIVE) {
$badges[$award->getBadgePHID()] = $badge;
}
$badges[$award->getBadgePHID()] = $badge;
}
if (count($badges)) {

View file

@ -18,11 +18,13 @@ final class PhabricatorPeopleQuery
private $nameLike;
private $nameTokens;
private $namePrefixes;
private $isEnrolledInMultiFactor;
private $needPrimaryEmail;
private $needProfile;
private $needProfileImage;
private $needAvailability;
private $needBadgeAwards;
private $cacheKeys = array();
public function withIDs(array $ids) {
@ -100,6 +102,11 @@ final class PhabricatorPeopleQuery
return $this;
}
public function withIsEnrolledInMultiFactor($enrolled) {
$this->isEnrolledInMultiFactor = $enrolled;
return $this;
}
public function needPrimaryEmail($need) {
$this->needPrimaryEmail = $need;
return $this;
@ -139,6 +146,18 @@ final class PhabricatorPeopleQuery
return $this;
}
public function needBadgeAwards($need) {
$cache_key = PhabricatorUserBadgesCacheType::KEY_BADGES;
if ($need) {
$this->cacheKeys[$cache_key] = true;
} else {
unset($this->cacheKeys[$cache_key]);
}
return $this;
}
public function newResultObject() {
return new PhabricatorUser();
}
@ -337,6 +356,13 @@ final class PhabricatorPeopleQuery
$this->nameLike);
}
if ($this->isEnrolledInMultiFactor !== null) {
$where[] = qsprintf(
$conn,
'user.isEnrolledInMultiFactor = %d',
(int)$this->isEnrolledInMultiFactor);
}
return $where;
}

View file

@ -18,7 +18,7 @@ final class PhabricatorPeopleSearchEngine
}
protected function buildCustomSearchFields() {
return array(
$fields = array(
id(new PhabricatorSearchStringListField())
->setLabel(pht('Usernames'))
->setKey('usernames')
@ -84,18 +84,36 @@ final class PhabricatorPeopleSearchEngine
pht(
'Pass true to find only users awaiting administrative approval, '.
'or false to omit these users.')),
id(new PhabricatorSearchDateField())
->setKey('createdStart')
->setLabel(pht('Joined After'))
->setDescription(
pht('Find user accounts created after a given time.')),
id(new PhabricatorSearchDateField())
->setKey('createdEnd')
->setLabel(pht('Joined Before'))
->setDescription(
pht('Find user accounts created before a given time.')),
);
$viewer = $this->requireViewer();
if ($viewer->getIsAdmin()) {
$fields[] = id(new PhabricatorSearchThreeStateField())
->setLabel(pht('Has MFA'))
->setKey('mfa')
->setOptions(
pht('(Show All)'),
pht('Show Only Users With MFA'),
pht('Hide Users With MFA'))
->setDescription(
pht(
'Pass true to find only users who are enrolled in MFA, or false '.
'to omit these users.'));
}
$fields[] = id(new PhabricatorSearchDateField())
->setKey('createdStart')
->setLabel(pht('Joined After'))
->setDescription(
pht('Find user accounts created after a given time.'));
$fields[] = id(new PhabricatorSearchDateField())
->setKey('createdEnd')
->setLabel(pht('Joined Before'))
->setDescription(
pht('Find user accounts created before a given time.'));
return $fields;
}
protected function getDefaultFieldOrder() {
@ -151,6 +169,19 @@ final class PhabricatorPeopleSearchEngine
$query->withIsApproved(!$map['needsApproval']);
}
if (idx($map, 'mfa') !== null) {
$viewer = $this->requireViewer();
if (!$viewer->getIsAdmin()) {
throw new PhabricatorSearchConstraintException(
pht(
'The "Has MFA" query constraint may only be used by '.
'administrators, to prevent attackers from using it to target '.
'weak accounts.'));
}
$query->withIsEnrolledInMultiFactor($map['mfa']);
}
if ($map['createdStart']) {
$query->withDateCreatedAfter($map['createdStart']);
}
@ -254,6 +285,12 @@ final class PhabricatorPeopleSearchEngine
$item->addIcon('fa-envelope-o', pht('Mailing List'));
}
if ($viewer->getIsAdmin()) {
if ($user->getIsEnrolledInMultiFactor()) {
$item->addIcon('fa-lock', pht('Has MFA'));
}
}
if ($viewer->getIsAdmin()) {
$user_id = $user->getID();
if ($is_approval) {

View file

@ -848,6 +848,11 @@ final class PhabricatorUser
return $this->requireCacheData($message_key);
}
public function getRecentBadgeAwards() {
$badges_key = PhabricatorUserBadgesCacheType::KEY_BADGES;
return $this->requireCacheData($badges_key);
}
public function getFullName() {
if (strlen($this->getRealName())) {
return $this->getUsername().' ('.$this->getRealName().')';

View file

@ -120,6 +120,7 @@ final class PhabricatorRepositoryPullEngine
pht(
'Updating the working copy for repository "%s".',
$repository->getDisplayName()));
if ($is_git) {
$this->verifyGitOrigin($repository);
$this->executeGitUpdate();
@ -157,7 +158,7 @@ final class PhabricatorRepositoryPullEngine
}
private function skipPull($message) {
$this->log('%s', $message);
$this->log($message);
$this->donePull();
}
@ -172,7 +173,7 @@ final class PhabricatorRepositoryPullEngine
}
private function logPull($message) {
$this->log('%s', $message);
$this->log($message);
}
private function donePull() {
@ -190,7 +191,7 @@ final class PhabricatorRepositoryPullEngine
}
private function installHook($path, array $hook_argv = array()) {
$this->log('%s', pht('Installing commit hook to "%s"...', $path));
$this->log(pht('Installing commit hook to "%s"...', $path));
$repository = $this->getRepository();
$identifier = $this->getHookContextIdentifier($repository);
@ -339,44 +340,36 @@ final class PhabricatorRepositoryPullEngine
throw new Exception($message);
}
$retry = false;
do {
// This is a local command, but needs credentials.
if ($repository->isWorkingCopyBare()) {
// For bare working copies, we need this magic incantation.
$future = $repository->getRemoteCommandFuture(
'fetch origin %s --prune',
'+refs/*:refs/*');
} else {
$future = $repository->getRemoteCommandFuture(
'fetch --all --prune');
}
$remote_refs = $this->loadGitRemoteRefs($repository);
$local_refs = $this->loadGitLocalRefs($repository);
if ($remote_refs === $local_refs) {
$this->log(
pht(
'Skipping fetch because local and remote refs are already '.
'identical.'));
return false;
}
$future->setCWD($path);
list($err, $stdout, $stderr) = $future->resolve();
$this->logRefDifferences($remote_refs, $local_refs);
if ($err && !$retry && $repository->canDestroyWorkingCopy()) {
$retry = true;
// Fix remote origin url if it doesn't match our configuration
$origin_url = $repository->execLocalCommand(
'config --get remote.origin.url');
$remote_uri = $repository->getRemoteURIEnvelope();
if ($origin_url != $remote_uri->openEnvelope()) {
$repository->execLocalCommand(
'remote set-url origin %P',
$remote_uri);
}
} else if ($err) {
throw new CommandException(
pht('Failed to fetch changes!'),
$future->getCommand(),
$err,
$stdout,
$stderr);
} else {
$retry = false;
}
} while ($retry);
// Force the "origin" URI to the configured value.
$repository->execxLocalCommand(
'remote set-url origin -- %P',
$repository->getRemoteURIEnvelope());
if ($repository->isWorkingCopyBare()) {
// For bare working copies, we need this magic incantation.
$future = $repository->getRemoteCommandFuture(
'fetch origin %s --prune',
'+refs/*:refs/*');
} else {
$future = $repository->getRemoteCommandFuture(
'fetch --all --prune');
}
$future
->setCWD($path)
->resolvex();
}
@ -396,6 +389,75 @@ final class PhabricatorRepositoryPullEngine
$this->installHook($root.$path);
}
private function loadGitRemoteRefs(PhabricatorRepository $repository) {
$remote_envelope = $repository->getRemoteURIEnvelope();
list($stdout) = $repository->execxRemoteCommand(
'ls-remote -- %P',
$remote_envelope);
$map = array();
$lines = phutil_split_lines($stdout, false);
foreach ($lines as $line) {
list($hash, $name) = preg_split('/\s+/', $line, 2);
// If the remote has a HEAD, just ignore it.
if ($name == 'HEAD') {
continue;
}
// If the remote ref is itself a remote ref, ignore it.
if (preg_match('(^refs/remotes/)', $name)) {
continue;
}
$map[$name] = $hash;
}
ksort($map);
return $map;
}
private function loadGitLocalRefs(PhabricatorRepository $repository) {
$refs = id(new DiffusionLowLevelGitRefQuery())
->setRepository($repository)
->execute();
$map = array();
foreach ($refs as $ref) {
$fields = $ref->getRawFields();
$map[idx($fields, 'refname')] = $ref->getCommitIdentifier();
}
ksort($map);
return $map;
}
private function logRefDifferences(array $remote, array $local) {
$all = $local + $remote;
$differences = array();
foreach ($all as $key => $ignored) {
$remote_ref = idx($remote, $key, pht('<null>'));
$local_ref = idx($local, $key, pht('<null>'));
if ($remote_ref !== $local_ref) {
$differences[] = pht(
'%s (remote: "%s", local: "%s")',
$key,
$remote_ref,
$local_ref);
}
}
$this->log(
pht(
"Updating repository after detecting ref differences:\n%s",
implode("\n", $differences)));
}
/* -( Pulling Mercurial Working Copies )----------------------------------- */

View file

@ -331,6 +331,8 @@ final class PhabricatorApplicationSearchController
'query parameters and correct errors.');
} catch (PhutilSearchQueryCompilerSyntaxException $ex) {
$exec_errors[] = $ex->getMessage();
} catch (PhabricatorSearchConstraintException $ex) {
$exec_errors[] = $ex->getMessage();
}
// The engine may have encountered additional errors during rendering;

View file

@ -0,0 +1,4 @@
<?php
final class PhabricatorSearchConstraintException
extends Exception {}

View file

@ -133,11 +133,13 @@ final class PhabricatorDashboardProfileMenuItem
$icon = $dashboard->getIcon();
$name = $this->getDisplayName($config);
$href = $this->getItemViewURI($config);
$action_href = '/dashboard/arrange/'.$dashboard->getID().'/';
$item = $this->newItem()
->setHref($href)
->setName($name)
->setIcon($icon);
->setIcon($icon)
->setActionIcon('fa-pencil', $action_href);
return array(
$item,

View file

@ -525,23 +525,18 @@ class PhabricatorApplicationTransactionCommentView extends AphrontView {
return null;
}
$awards = id(new PhabricatorBadgesAwardQuery())
->setViewer($this->getUser())
->withRecipientPHIDs(array($user->getPHID()))
->setLimit(2)
->execute();
// Pull Badges from UserCache
$badges = $user->getRecentBadgeAwards();
$badge_view = null;
if ($awards) {
$badges = mpull($awards, 'getBadge');
if ($badges) {
$badge_list = array();
foreach ($badges as $badge) {
$badge_view = id(new PHUIBadgeMiniView())
->setIcon($badge->getIcon())
->setQuality($badge->getQuality())
->setHeader($badge->getName())
->setIcon($badge['icon'])
->setQuality($badge['quality'])
->setHeader($badge['name'])
->setTipDirection('E')
->setHref('/badges/view/'.$badge->getID());
->setHref('/badges/view/'.$badge['id'].'/');
$badge_list[] = $badge_view;
}

View file

@ -97,7 +97,7 @@ final class PHUIBadgeExample extends PhabricatorUIExample {
$badges2[] = id(new PHUIBadgeView())
->setIcon('fa-user')
->setHeader(pht('Adminstrator'))
->setHeader(pht('Administrator'))
->setSubhead(pht('Drew the short stick'))
->setQuality(PhabricatorBadgesQuality::LEGENDARY)
->setSource(pht('People (automatic)'))

View file

@ -414,21 +414,44 @@ final class PhabricatorEnv extends Phobject {
return rtrim($production_domain, '/').$path;
}
public static function getAllowedURIs($path) {
$uri = new PhutilURI($path);
if ($uri->getDomain()) {
return $path;
public static function isSelfURI($raw_uri) {
$uri = new PhutilURI($raw_uri);
$host = $uri->getDomain();
if (!strlen($host)) {
return false;
}
$allowed_uris = self::getEnvConfig('phabricator.allowed-uris');
$return = array();
foreach ($allowed_uris as $allowed_uri) {
$return[] = rtrim($allowed_uri, '/').$path;
}
$host = phutil_utf8_strtolower($host);
return $return;
$self_map = self::getSelfURIMap();
return isset($self_map[$host]);
}
private static function getSelfURIMap() {
$self_uris = array();
$self_uris[] = self::getProductionURI('/');
$self_uris[] = self::getURI('/');
$allowed_uris = self::getEnvConfig('phabricator.allowed-uris');
foreach ($allowed_uris as $allowed_uri) {
$self_uris[] = $allowed_uri;
}
$self_map = array();
foreach ($self_uris as $self_uri) {
$host = id(new PhutilURI($self_uri))->getDomain();
if (!strlen($host)) {
continue;
}
$host = phutil_utf8_strtolower($host);
$self_map[$host] = $host;
}
return $self_map;
}
/**
* Get the fully-qualified production URI for a static resource path.

View file

@ -218,4 +218,39 @@ final class PhabricatorEnvTestCase extends PhabricatorTestCase {
$this->assertFalse($caught instanceof Exception);
}
public function testSelfURI() {
$base_uri = 'https://allowed.example.com/';
$allowed_uris = array(
'https://old.example.com/',
);
$env = PhabricatorEnv::beginScopedEnv();
$env->overrideEnvConfig('phabricator.base-uri', $base_uri);
$env->overrideEnvConfig('phabricator.allowed-uris', $allowed_uris);
$map = array(
'https://allowed.example.com/' => true,
'https://allowed.example.com' => true,
'https://allowed.EXAMPLE.com' => true,
'http://allowed.example.com/' => true,
'https://allowed.example.com/path/to/resource.png' => true,
'https://old.example.com/' => true,
'https://old.example.com' => true,
'https://old.EXAMPLE.com' => true,
'http://old.example.com/' => true,
'https://old.example.com/path/to/resource.png' => true,
'https://other.example.com/' => false,
);
foreach ($map as $input => $expect) {
$this->assertEqual(
$expect,
PhabricatorEnv::isSelfURI($input),
pht('Is self URI? %s', $input));
}
}
}

View file

@ -31,6 +31,8 @@ final class PHUIListItemView extends AphrontTagView {
private $icons = array();
private $openInNewWindow = false;
private $tooltip;
private $actionIcon;
private $actionIconHref;
public function setOpenInNewWindow($open_in_new_window) {
$this->openInNewWindow = $open_in_new_window;
@ -154,6 +156,12 @@ final class PHUIListItemView extends AphrontTagView {
return $this->name;
}
public function setActionIcon($icon, $href) {
$this->actionIcon = $icon;
$this->actionIconHref = $href;
return $this;
}
public function setIsExternal($is_external) {
$this->isExternal = $is_external;
return $this;
@ -207,6 +215,10 @@ final class PHUIListItemView extends AphrontTagView {
$classes[] = $this->statusColor;
}
if ($this->actionIcon) {
$classes[] = 'phui-list-item-has-action-icon';
}
return array(
'class' => implode(' ', $classes),
);
@ -311,9 +323,23 @@ final class PHUIListItemView extends AphrontTagView {
$classes[] = 'phui-list-item-indented';
}
$action_link = null;
if ($this->actionIcon) {
$action_icon = id(new PHUIIconView())
->setIcon($this->actionIcon)
->addClass('phui-list-item-action-icon');
$action_link = phutil_tag(
'a',
array(
'href' => $this->actionIconHref,
'class' => 'phui-list-item-action-href',
),
$action_icon);
}
$icons = $this->getIcons();
return javelin_tag(
$list_item = javelin_tag(
$this->href ? 'a' : 'div',
array(
'href' => $this->href,
@ -329,6 +355,8 @@ final class PHUIListItemView extends AphrontTagView {
$this->renderChildren(),
$name,
));
return array($list_item, $action_link);
}
}

View file

@ -220,7 +220,6 @@ final class PHUITimelineView extends AphrontView {
}
$user_phid_type = PhabricatorPeopleUserPHIDType::TYPECONST;
$badge_edge_type = PhabricatorRecipientHasBadgeEdgeType::EDGECONST;
$user_phids = array();
foreach ($events as $key => $event) {
@ -244,38 +243,26 @@ final class PHUITimelineView extends AphrontView {
return;
}
$awards = id(new PhabricatorBadgesAwardQuery())
->setViewer($this->getViewer())
->withRecipientPHIDs($user_phids)
$users = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->withPHIDs($user_phids)
->needBadgeAwards(true)
->execute();
$awards = mgroup($awards, 'getRecipientPHID');
$users = mpull($users, null, 'getPHID');
foreach ($events as $event) {
$author_awards = idx($awards, $event->getAuthorPHID(), array());
$badges = array();
foreach ($author_awards as $award) {
$badge = $award->getBadge();
if ($badge->getStatus() == PhabricatorBadgesBadge::STATUS_ACTIVE) {
$badges[$award->getBadgePHID()] = $badge;
}
$user_phid = $event->getAuthorPHID();
if (!array_key_exists($user_phid, $users)) {
continue;
}
// TODO: Pick the "best" badges in some smart way. For now, just pick
// the first two.
$badges = array_slice($badges, 0, 2);
$badges = $users[$user_phid]->getRecentBadgeAwards();
foreach ($badges as $badge) {
$badge_view = id(new PHUIBadgeMiniView())
->setIcon($badge->getIcon())
->setQuality($badge->getQuality())
->setHeader($badge->getName())
->setIcon($badge['icon'])
->setQuality($badge['quality'])
->setHeader($badge['name'])
->setTipDirection('E')
->setHref('/badges/view/'.$badge->getID());
->setHref('/badges/view/'.$badge['id'].'/');
$event->addBadge($badge_view);
}
}

View file

@ -3,7 +3,7 @@
*/
.aphront-dialog-view {
width: 540px;
width: 560px;
margin: 32px auto 16px;
border: 1px solid {$lightblueborder};
border-radius: 3px;
@ -32,7 +32,7 @@
}
.aphront-dialog-view-width-form {
width: 600px;
width: 640px;
}
.aphront-dialog-view-width-full {

View file

@ -181,13 +181,14 @@
border: none;
}
.device .remarkup-assist-button,
.device .remarkup-assist-separator {
.device .conpherence-message-pane .remarkup-assist-button,
.device .conpherence-message-pane .remarkup-assist-separator {
display: none;
}
.device .remarkup-assist-button.remarkup-assist-upload {
display: block;
.device .conpherence-message-pane
.remarkup-assist-button.remarkup-assist-upload {
display: block;
}
.device .conpherence-message-pane .phui-form-view {
@ -343,7 +344,7 @@
padding: 2px 0 8px 0;
}
.conpherence-message-pane .aphront-form-control {
body .conpherence-message-pane .aphront-form-control {
padding: 0;
}

View file

@ -208,14 +208,13 @@
table.aphront-form-control-radio-layout,
table.aphront-form-control-checkbox-layout {
margin-top: 3px;
margin-top: 4px !important;
font-size: {$normalfontsize};
}
table.aphront-form-control-radio-layout th {
padding-top: 3px;
padding-left: 8px;
padding-bottom: 4px;
padding-bottom: 8px;
font-weight: bold;
color: {$darkgreytext};
}

View file

@ -2,6 +2,10 @@
* @provides phui-list-view-css
*/
.phui-list-item-view {
position: relative;
}
.phui-list-item-header,
.phui-list-item-header a {
color: {$bluetext};
@ -188,3 +192,39 @@
margin-top: 16px;
border-top: 1px solid {$thinblueborder};
}
/* - Action Icon ----------------------------------------------------------- */
.phui-list-item-has-action-icon .phui-list-item-action-href {
position: absolute;
width: 28px;
top: 0;
right: 0;
bottom: 0;
text-align: center;
line-height: 28px;
background-color: transparent;
display: none;
}
.phui-list-item-has-action-icon.phui-list-item-selected .phui-list-item-href {
padding-right: 32px;
}
.phui-list-item-has-action-icon.phui-list-item-selected
.phui-list-item-action-href {
display: block;
}
.phui-list-item-has-action-icon .phui-list-item-action-href:hover {
background-color: rgba({$alphablack},.05);
}
.phui-list-item-has-action-icon .phui-list-item-action-icon {
opacity: 0.5;
}
.phui-list-item-has-action-icon .phui-list-item-action-href:hover
.phui-list-item-action-icon {
opacity: 1;
}