diff --git a/resources/sql/autopatches/20190121.contact.01.primary.sql b/resources/sql/autopatches/20190121.contact.01.primary.sql new file mode 100644 --- /dev/null +++ b/resources/sql/autopatches/20190121.contact.01.primary.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_auth.auth_contactnumber + ADD isPrimary BOOL NOT NULL; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2207,6 +2207,8 @@ 'PhabricatorAuthContactNumberEditor' => 'applications/auth/editor/PhabricatorAuthContactNumberEditor.php', 'PhabricatorAuthContactNumberNumberTransaction' => 'applications/auth/xaction/PhabricatorAuthContactNumberNumberTransaction.php', 'PhabricatorAuthContactNumberPHIDType' => 'applications/auth/phid/PhabricatorAuthContactNumberPHIDType.php', + 'PhabricatorAuthContactNumberPrimaryController' => 'applications/auth/controller/contact/PhabricatorAuthContactNumberPrimaryController.php', + 'PhabricatorAuthContactNumberPrimaryTransaction' => 'applications/auth/xaction/PhabricatorAuthContactNumberPrimaryTransaction.php', 'PhabricatorAuthContactNumberQuery' => 'applications/auth/query/PhabricatorAuthContactNumberQuery.php', 'PhabricatorAuthContactNumberStatusTransaction' => 'applications/auth/xaction/PhabricatorAuthContactNumberStatusTransaction.php', 'PhabricatorAuthContactNumberTransaction' => 'applications/auth/storage/PhabricatorAuthContactNumberTransaction.php', @@ -7912,6 +7914,8 @@ 'PhabricatorAuthContactNumberEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorAuthContactNumberNumberTransaction' => 'PhabricatorAuthContactNumberTransactionType', 'PhabricatorAuthContactNumberPHIDType' => 'PhabricatorPHIDType', + 'PhabricatorAuthContactNumberPrimaryController' => 'PhabricatorAuthContactNumberController', + 'PhabricatorAuthContactNumberPrimaryTransaction' => 'PhabricatorAuthContactNumberTransactionType', 'PhabricatorAuthContactNumberQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorAuthContactNumberStatusTransaction' => 'PhabricatorAuthContactNumberTransactionType', 'PhabricatorAuthContactNumberTransaction' => 'PhabricatorModularTransaction', diff --git a/src/applications/auth/application/PhabricatorAuthApplication.php b/src/applications/auth/application/PhabricatorAuthApplication.php --- a/src/applications/auth/application/PhabricatorAuthApplication.php +++ b/src/applications/auth/application/PhabricatorAuthApplication.php @@ -113,6 +113,8 @@ 'PhabricatorAuthContactNumberViewController', '(?Pdisable|enable)/(?P[1-9]\d*)/' => 'PhabricatorAuthContactNumberDisableController', + 'primary/(?P[1-9]\d*)/' => + 'PhabricatorAuthContactNumberPrimaryController', ), ), diff --git a/src/applications/auth/controller/contact/PhabricatorAuthContactNumberPrimaryController.php b/src/applications/auth/controller/contact/PhabricatorAuthContactNumberPrimaryController.php new file mode 100644 --- /dev/null +++ b/src/applications/auth/controller/contact/PhabricatorAuthContactNumberPrimaryController.php @@ -0,0 +1,78 @@ +getViewer(); + $id = $request->getURIData('id'); + + $number = id(new PhabricatorAuthContactNumberQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$number) { + return new Aphront404Response(); + } + + $id = $number->getID(); + $cancel_uri = $number->getURI(); + + if ($number->isDisabled()) { + return $this->newDialog() + ->setTitle(pht('Number Disabled')) + ->appendParagraph( + pht( + 'You can not make a disabled number your primary contact number.')) + ->addCancelButton($cancel_uri); + } + + if ($number->getIsPrimary()) { + return $this->newDialog() + ->setTitle(pht('Number Already Primary')) + ->appendParagraph( + pht( + 'This contact number is already your primary contact number.')) + ->addCancelButton($cancel_uri); + } + + if ($request->isFormPost()) { + $xactions = array(); + + $xactions[] = id(new PhabricatorAuthContactNumberTransaction()) + ->setTransactionType( + PhabricatorAuthContactNumberPrimaryTransaction::TRANSACTIONTYPE) + ->setNewValue(true); + + $editor = id(new PhabricatorAuthContactNumberEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true); + + $editor->applyTransactions($number, $xactions); + + return id(new AphrontRedirectResponse())->setURI($cancel_uri); + } + + $number_display = phutil_tag( + 'strong', + array(), + $number->getDisplayName()); + + return $this->newDialog() + ->setTitle(pht('Set Primary Contact Number')) + ->appendParagraph( + pht( + 'Designate %s as your primary contact number?', + $number_display)) + ->addSubmitButton(pht('Make Primary')) + ->addCancelButton($cancel_uri); + } + +} diff --git a/src/applications/auth/controller/contact/PhabricatorAuthContactNumberViewController.php b/src/applications/auth/controller/contact/PhabricatorAuthContactNumberViewController.php --- a/src/applications/auth/controller/contact/PhabricatorAuthContactNumberViewController.php +++ b/src/applications/auth/controller/contact/PhabricatorAuthContactNumberViewController.php @@ -56,6 +56,8 @@ if ($number->isDisabled()) { $view->setStatus('fa-ban', 'red', pht('Disabled')); + } else if ($number->getIsPrimary()) { + $view->setStatus('fa-certificate', 'blue', pht('Primary')); } return $view; @@ -96,17 +98,26 @@ ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); - if ($number->isDisabled()) { $disable_uri = $this->getApplicationURI("contact/enable/{$id}/"); $disable_name = pht('Enable Contact Number'); $disable_icon = 'fa-check'; + $can_primary = false; } else { $disable_uri = $this->getApplicationURI("contact/disable/{$id}/"); $disable_name = pht('Disable Contact Number'); $disable_icon = 'fa-ban'; + $can_primary = !$number->getIsPrimary(); } + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Make Primary Number')) + ->setIcon('fa-certificate') + ->setHref($this->getApplicationURI("contact/primary/{$id}/")) + ->setDisabled(!$can_primary) + ->setWorkflow(true)); + $curtain->addAction( id(new PhabricatorActionView()) ->setName($disable_name) diff --git a/src/applications/auth/storage/PhabricatorAuthContactNumber.php b/src/applications/auth/storage/PhabricatorAuthContactNumber.php --- a/src/applications/auth/storage/PhabricatorAuthContactNumber.php +++ b/src/applications/auth/storage/PhabricatorAuthContactNumber.php @@ -12,6 +12,7 @@ protected $contactNumber; protected $uniqueKey; protected $status; + protected $isPrimary; protected $properties = array(); const STATUS_ACTIVE = 'active'; @@ -27,6 +28,7 @@ 'contactNumber' => 'text255', 'status' => 'text32', 'uniqueKey' => 'bytes12?', + 'isPrimary' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'key_object' => array( @@ -43,7 +45,8 @@ public static function initializeNewContactNumber($object) { return id(new self()) ->setStatus(self::STATUS_ACTIVE) - ->setObjectPHID($object->getPHID()); + ->setObjectPHID($object->getPHID()) + ->setIsPrimary(0); } public function getPHIDType() { @@ -73,8 +76,14 @@ ->setTooltip(pht('Disabled')); } + if ($this->getIsPrimary()) { + return id(new PHUIIconView()) + ->setIcon('fa-certificate', 'blue') + ->setTooltip(pht('Primary Number')); + } + return id(new PHUIIconView()) - ->setIcon('fa-mobile', 'green') + ->setIcon('fa-hashtag', 'bluegrey') ->setTooltip(pht('Active Phone Number')); } @@ -101,7 +110,61 @@ $this->uniqueKey = $this->newUniqueKey(); } - return parent::save(); + parent::save(); + + return $this->updatePrimaryContactNumber(); + } + + private function updatePrimaryContactNumber() { + // Update the "isPrimary" column so that at most one number is primary for + // each user, and no disabled number is primary. + + $conn = $this->establishConnection('w'); + $this_id = (int)$this->getID(); + + if ($this->getIsPrimary() && !$this->isDisabled()) { + // If we're trying to make this number primary and it's active, great: + // make this number the primary number. + $primary_id = $this_id; + } else { + // If we aren't trying to make this number primary or it is disabled, + // pick another number to make primary if we can. A number must be active + // to become primary. + + // If there are multiple active numbers, pick the oldest one currently + // marked primary (usually, this should mean that we just keep the + // current primary number as primary). + + // If none are marked primary, just pick the oldest one. + $primary_row = queryfx_one( + $conn, + 'SELECT id FROM %R + WHERE objectPHID = %s AND status = %s + ORDER BY isPrimary DESC, id ASC + LIMIT 1', + $this, + $this->getObjectPHID(), + self::STATUS_ACTIVE); + if ($primary_row) { + $primary_id = (int)$primary_row['id']; + } else { + $primary_id = -1; + } + } + + // Set the chosen number to primary, and all other numbers to nonprimary. + + queryfx( + $conn, + 'UPDATE %R SET isPrimary = IF(id = %d, 1, 0) + WHERE objectPHID = %s', + $this, + $primary_id, + $this->getObjectPHID()); + + $this->setIsPrimary((int)($primary_id === $this_id)); + + return $this; } public static function getStatusNameMap() { @@ -119,6 +182,15 @@ ); } + public function getSortVector() { + // Sort the primary number first, then active numbers, then disabled + // numbers. In each group, sort from oldest to newest. + return id(new PhutilSortVector()) + ->addInt($this->getIsPrimary() ? 0 : 1) + ->addInt($this->isDisabled() ? 1 : 0) + ->addInt($this->getID()); + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/auth/xaction/PhabricatorAuthContactNumberPrimaryTransaction.php b/src/applications/auth/xaction/PhabricatorAuthContactNumberPrimaryTransaction.php new file mode 100644 --- /dev/null +++ b/src/applications/auth/xaction/PhabricatorAuthContactNumberPrimaryTransaction.php @@ -0,0 +1,49 @@ +getIsPrimary(); + } + + public function applyInternalEffects($object, $value) { + $object->setIsPrimary((int)$value); + } + + public function getTitle() { + return pht( + '%s made this the primary contact number.', + $this->renderAuthor()); + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + foreach ($xactions as $xaction) { + $new_value = $xaction->getNewValue(); + + if (!$new_value) { + $errors[] = $this->newInvalidError( + pht( + 'To choose a different primary contact number, make that '. + 'number primary (instead of trying to demote this one).'), + $xaction); + continue; + } + + if ($object->isDisabled()) { + $errors[] = $this->newInvalidError( + pht( + 'You can not make a disabled number a primary contact number.'), + $xaction); + continue; + } + } + + return $errors; + } + +} diff --git a/src/applications/settings/panel/PhabricatorContactNumbersSettingsPanel.php b/src/applications/settings/panel/PhabricatorContactNumbersSettingsPanel.php --- a/src/applications/settings/panel/PhabricatorContactNumbersSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorContactNumbersSettingsPanel.php @@ -12,7 +12,7 @@ } public function getPanelMenuIcon() { - return 'fa-mobile'; + return 'fa-hashtag'; } public function getPanelGroupKey() { @@ -31,9 +31,19 @@ ->setViewer($viewer) ->withObjectPHIDs(array($user->getPHID())) ->execute(); + $numbers = msortv($numbers, 'getSortVector'); $rows = array(); + $row_classes = array(); foreach ($numbers as $number) { + if ($number->getIsPrimary()) { + $primary_display = pht('Primary'); + $row_classes[] = 'highlighted'; + } else { + $primary_display = null; + $row_classes[] = null; + } + $rows[] = array( $number->newIconView(), phutil_tag( @@ -42,6 +52,7 @@ 'href' => $number->getURI(), ), $number->getDisplayName()), + $primary_display, phabricator_datetime($number->getDateCreated(), $viewer), ); } @@ -49,16 +60,19 @@ $table = id(new AphrontTableView($rows)) ->setNoDataString( pht("You haven't added any contact numbers to your account.")) + ->setRowClasses($row_classes) ->setHeaders( array( null, pht('Number'), + pht('Status'), pht('Created'), )) ->setColumnClasses( array( null, 'wide pri', + null, 'right', ));