diff --git a/resources/sql/autopatches/20191113.identity.03.unassigned.sql b/resources/sql/autopatches/20191113.identity.03.unassigned.sql new file mode 100644 index 0000000000..768ca1d909 --- /dev/null +++ b/resources/sql/autopatches/20191113.identity.03.unassigned.sql @@ -0,0 +1,3 @@ +UPDATE {$NAMESPACE}_repository.repository_identity + SET currentEffectiveUserPHID = NULL + WHERE currentEffectiveUserPHID = 'unassigned()'; diff --git a/src/applications/diffusion/controller/DiffusionIdentityViewController.php b/src/applications/diffusion/controller/DiffusionIdentityViewController.php index 0ac131daf1..1c78ec5992 100644 --- a/src/applications/diffusion/controller/DiffusionIdentityViewController.php +++ b/src/applications/diffusion/controller/DiffusionIdentityViewController.php @@ -1,138 +1,141 @@ getViewer(); $id = $request->getURIData('id'); $identity = id(new PhabricatorRepositoryIdentityQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->executeOne(); if (!$identity) { return new Aphront404Response(); } $title = pht('Identity %d', $identity->getID()); $curtain = $this->buildCurtain($identity); $header = id(new PHUIHeaderView()) ->setUser($viewer) ->setHeader($identity->getIdentityShortName()) ->setHeaderIcon('fa-globe'); $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb( + pht('Identities'), + $this->getApplicationURI('identity/')); $crumbs->addTextCrumb($identity->getObjectName()); $crumbs->setBorder(true); $timeline = $this->buildTransactionTimeline( $identity, new PhabricatorRepositoryIdentityTransactionQuery()); $timeline->setShouldTerminate(true); $properties = $this->buildPropertyList($identity); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setCurtain($curtain) ->setMainColumn(array( $properties, $timeline, )); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) ->appendChild( array( $view, )); } private function buildCurtain(PhabricatorRepositoryIdentity $identity) { $viewer = $this->getViewer(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $identity, PhabricatorPolicyCapability::CAN_EDIT); $id = $identity->getID(); $edit_uri = $this->getApplicationURI("identity/edit/{$id}/"); $curtain = $this->newCurtainView($identity); $curtain->addAction( id(new PhabricatorActionView()) ->setIcon('fa-pencil') ->setName(pht('Edit Identity')) ->setHref($edit_uri) ->setWorkflow(!$can_edit) ->setDisabled(!$can_edit)); return $curtain; } private function buildPropertyList( PhabricatorRepositoryIdentity $identity) { $viewer = $this->getViewer(); $properties = id(new PHUIPropertyListView()) ->setViewer($viewer); $properties->addProperty( pht('Email Address'), $identity->getEmailAddress()); $effective_phid = $identity->getCurrentEffectiveUserPHID(); $automatic_phid = $identity->getAutomaticGuessedUserPHID(); $manual_phid = $identity->getManuallySetUserPHID(); if ($effective_phid) { $tag = id(new PHUITagView()) ->setType(PHUITagView::TYPE_SHADE) ->setColor('green') ->setIcon('fa-check') ->setName('Assigned'); } else { $tag = id(new PHUITagView()) ->setType(PHUITagView::TYPE_SHADE) ->setColor('indigo') ->setIcon('fa-bomb') ->setName('Unassigned'); } $properties->addProperty( pht('Effective User'), $this->buildPropertyValue($effective_phid)); $properties->addProperty( pht('Automatically Detected User'), $this->buildPropertyValue($automatic_phid)); $properties->addProperty( pht('Assigned To'), $this->buildPropertyValue($manual_phid)); $header = id(new PHUIHeaderView()) ->setHeader(array(pht('Identity Assignments'), $tag)); return id(new PHUIObjectBoxView()) ->setHeader($header) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->addPropertyList($properties); } private function buildPropertyValue($value) { $viewer = $this->getViewer(); if ($value == DiffusionIdentityUnassignedDatasource::FUNCTION_TOKEN) { return phutil_tag('em', array(), pht('Explicitly Unassigned')); } else if (!$value) { return phutil_tag('em', array(), pht('None')); } else { return $viewer->renderHandle($value); } } } diff --git a/src/applications/diffusion/query/DiffusionRepositoryIdentitySearchEngine.php b/src/applications/diffusion/query/DiffusionRepositoryIdentitySearchEngine.php index 4d10ee44e0..f41b6b89a4 100644 --- a/src/applications/diffusion/query/DiffusionRepositoryIdentitySearchEngine.php +++ b/src/applications/diffusion/query/DiffusionRepositoryIdentitySearchEngine.php @@ -1,108 +1,165 @@ setLabel(pht('Matching Users')) + ->setKey('effectivePHIDs') + ->setAliases( + array( + 'effective', + 'effectivePHID', + )) + ->setDescription(pht('Search for identities by effective user.')), id(new DiffusionIdentityAssigneeSearchField()) ->setLabel(pht('Assigned To')) - ->setKey('assignee') - ->setDescription(pht('Search for identities by assignee.')), + ->setKey('assignedPHIDs') + ->setAliases( + array( + 'assigned', + 'assignedPHID', + )) + ->setDescription(pht('Search for identities by explicit assignee.')), id(new PhabricatorSearchTextField()) ->setLabel(pht('Identity Contains')) ->setKey('match') ->setDescription(pht('Search for identities by substring.')), id(new PhabricatorSearchThreeStateField()) - ->setLabel(pht('Is Assigned')) + ->setLabel(pht('Has Matching User')) ->setKey('hasEffectivePHID') ->setOptions( pht('(Show All)'), - pht('Show Only Assigned Identities'), - pht('Show Only Unassigned Identities')), + pht('Show Identities With Matching Users'), + pht('Show Identities Without Matching Users')), ); } protected function buildQueryFromParameters(array $map) { $query = $this->newQuery(); if ($map['hasEffectivePHID'] !== null) { $query->withHasEffectivePHID($map['hasEffectivePHID']); } if ($map['match'] !== null) { $query->withIdentityNameLike($map['match']); } - if ($map['assignee']) { - $query->withAssigneePHIDs($map['assignee']); + if ($map['assignedPHIDs']) { + $query->withAssignedPHIDs($map['assignedPHIDs']); + } + + if ($map['effectivePHIDs']) { + $query->withEffectivePHIDs($map['effectivePHIDs']); } return $query; } protected function getURI($path) { return '/diffusion/identity/'.$path; } protected function getBuiltinQueryNames() { $names = array( 'all' => pht('All Identities'), ); return $names; } public function buildSavedQueryFromBuiltin($query_key) { $query = $this->newSavedQuery(); $query->setQueryKey($query_key); switch ($query_key) { case 'all': return $query; } return parent::buildSavedQueryFromBuiltin($query_key); } protected function renderResultList( array $identities, PhabricatorSavedQuery $query, array $handles) { assert_instances_of($identities, 'PhabricatorRepositoryIdentity'); $viewer = $this->requireViewer(); - $list = new PHUIObjectItemListView(); - $list->setUser($viewer); + $list = id(new PHUIObjectItemListView()) + ->setViewer($viewer); + + $phids = array(); + foreach ($identities as $identity) { + $phids[] = $identity->getCurrentEffectiveUserPHID(); + $phids[] = $identity->getManuallySetUserPHID(); + } + + $handles = $viewer->loadHandles($phids); + + $unassigned = DiffusionIdentityUnassignedDatasource::FUNCTION_TOKEN; + foreach ($identities as $identity) { $item = id(new PHUIObjectItemView()) - ->setObjectName(pht('Identity %d', $identity->getID())) + ->setObjectName($identity->getObjectName()) ->setHeader($identity->getIdentityShortName()) ->setHref($identity->getURI()) ->setObject($identity); + $status_icon = 'fa-circle-o grey'; + + $effective_phid = $identity->getCurrentEffectiveUserPHID(); + if ($effective_phid) { + $item->addIcon( + 'fa-id-badge', + pht('Matches User: %s', $handles[$effective_phid]->getName())); + + $status_icon = 'fa-id-badge'; + } + + $assigned_phid = $identity->getManuallySetUserPHID(); + if ($assigned_phid) { + if ($assigned_phid === $unassigned) { + $item->addIcon( + 'fa-ban', + pht('Explicitly Unassigned')); + $status_icon = 'fa-ban'; + } else { + $item->addIcon( + 'fa-user', + pht('Assigned To: %s', $handles[$assigned_phid]->getName())); + $status_icon = 'fa-user'; + } + } + + $item->setStatusIcon($status_icon); + $list->addItem($item); } $result = new PhabricatorApplicationSearchResultView(); $result->setObjectList($list); $result->setNoDataString(pht('No Identities found.')); return $result; } } diff --git a/src/applications/repository/query/PhabricatorRepositoryIdentityQuery.php b/src/applications/repository/query/PhabricatorRepositoryIdentityQuery.php index 6ddf8d57c1..14c2a75f80 100644 --- a/src/applications/repository/query/PhabricatorRepositoryIdentityQuery.php +++ b/src/applications/repository/query/PhabricatorRepositoryIdentityQuery.php @@ -1,130 +1,143 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withIdentityNames(array $names) { $this->identityNames = $names; return $this; } public function withIdentityNameLike($name_like) { $this->identityNameLike = $name_like; return $this; } public function withEmailAddresses(array $addresses) { $this->emailAddresses = $addresses; return $this; } - public function withAssigneePHIDs(array $assignees) { - $this->assigneePHIDs = $assignees; + public function withAssignedPHIDs(array $assigned) { + $this->assignedPHIDs = $assigned; + return $this; + } + + public function withEffectivePHIDs(array $effective) { + $this->effectivePHIDs = $effective; return $this; } public function withHasEffectivePHID($has_effective_phid) { $this->hasEffectivePHID = $has_effective_phid; return $this; } public function newResultObject() { return new PhabricatorRepositoryIdentity(); } protected function getPrimaryTableAlias() { return 'repository_identity'; } protected function loadPage() { return $this->loadStandardPage($this->newResultObject()); } protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { $where = parent::buildWhereClauseParts($conn); if ($this->ids !== null) { $where[] = qsprintf( $conn, 'repository_identity.id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn, 'repository_identity.phid IN (%Ls)', $this->phids); } - if ($this->assigneePHIDs !== null) { + if ($this->assignedPHIDs !== null) { + $where[] = qsprintf( + $conn, + 'repository_identity.manuallySetUserPHID IN (%Ls)', + $this->assignedPHIDs); + } + + if ($this->effectivePHIDs !== null) { $where[] = qsprintf( $conn, 'repository_identity.currentEffectiveUserPHID IN (%Ls)', - $this->assigneePHIDs); + $this->effectivePHIDs); } if ($this->hasEffectivePHID !== null) { if ($this->hasEffectivePHID) { $where[] = qsprintf( $conn, 'repository_identity.currentEffectiveUserPHID IS NOT NULL'); } else { $where[] = qsprintf( $conn, 'repository_identity.currentEffectiveUserPHID IS NULL'); } } if ($this->identityNames !== null) { $name_hashes = array(); foreach ($this->identityNames as $name) { $name_hashes[] = PhabricatorHash::digestForIndex($name); } $where[] = qsprintf( $conn, 'repository_identity.identityNameHash IN (%Ls)', $name_hashes); } if ($this->emailAddresses !== null) { $where[] = qsprintf( $conn, 'repository_identity.emailAddress IN (%Ls)', $this->emailAddresses); } if ($this->identityNameLike != null) { $where[] = qsprintf( $conn, 'repository_identity.identityNameRaw LIKE %~', $this->identityNameLike); } return $where; } public function getQueryApplicationClass() { return 'PhabricatorDiffusionApplication'; } } diff --git a/src/applications/repository/storage/PhabricatorRepositoryIdentity.php b/src/applications/repository/storage/PhabricatorRepositoryIdentity.php index c33b296fdc..74fbd06544 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryIdentity.php +++ b/src/applications/repository/storage/PhabricatorRepositoryIdentity.php @@ -1,159 +1,166 @@ true, self::CONFIG_BINARY => array( 'identityNameRaw' => true, ), self::CONFIG_COLUMN_SCHEMA => array( 'identityNameHash' => 'bytes12', 'identityNameEncoding' => 'text16?', 'automaticGuessedUserPHID' => 'phid?', 'manuallySetUserPHID' => 'phid?', 'currentEffectiveUserPHID' => 'phid?', 'emailAddress' => 'sort255?', ), self::CONFIG_KEY_SCHEMA => array( 'key_identity' => array( 'columns' => array('identityNameHash'), 'unique' => true, ), 'key_email' => array( 'columns' => array('emailAddress(64)'), ), ), ) + parent::getConfiguration(); } public function getPHIDType() { return PhabricatorRepositoryIdentityPHIDType::TYPECONST; } public function setIdentityName($name_raw) { $this->setIdentityNameRaw($name_raw); $this->setIdentityNameHash(PhabricatorHash::digestForIndex($name_raw)); $this->setIdentityNameEncoding($this->detectEncodingForStorage($name_raw)); return $this; } public function getIdentityName() { return $this->getUTF8StringFromStorage( $this->getIdentityNameRaw(), $this->getIdentityNameEncoding()); } public function getIdentityEmailAddress() { $address = new PhutilEmailAddress($this->getIdentityName()); return $address->getAddress(); } public function getIdentityDisplayName() { $address = new PhutilEmailAddress($this->getIdentityName()); return $address->getDisplayName(); } public function getIdentityShortName() { // TODO return $this->getIdentityName(); } public function getObjectName() { return pht('Identity %d', $this->getID()); } public function getURI() { return '/diffusion/identity/view/'.$this->getID().'/'; } public function hasEffectiveUser() { return ($this->currentEffectiveUserPHID != null); } public function getIdentityDisplayPHID() { if ($this->hasEffectiveUser()) { return $this->getCurrentEffectiveUserPHID(); } else { return $this->getPHID(); } } public function save() { if ($this->manuallySetUserPHID) { - $this->currentEffectiveUserPHID = $this->manuallySetUserPHID; + $unassigned = DiffusionIdentityUnassignedDatasource::FUNCTION_TOKEN; + if ($this->manuallySetUserPHID === $unassigned) { + $effective_phid = null; + } else { + $effective_phid = $this->manuallySetUserPHID; + } } else { - $this->currentEffectiveUserPHID = $this->automaticGuessedUserPHID; + $effective_phid = $this->automaticGuessedUserPHID; } + $this->setCurrentEffectiveUserPHID($effective_phid); + $email_address = $this->getIdentityEmailAddress(); // Raw identities are unrestricted binary data, and may consequently // have arbitrarily long, binary email address information. We can't // store this kind of information in the "emailAddress" column, which // has column type "sort255". // This kind of address almost certainly not legitimate and users can // manually set the target of the identity, so just discard it rather // than trying especially hard to make it work. $byte_limit = $this->getColumnMaximumByteLength('emailAddress'); $email_address = phutil_utf8ize($email_address); if (strlen($email_address) > $byte_limit) { $email_address = null; } $this->setEmailAddress($email_address); return parent::save(); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { return PhabricatorPolicies::getMostOpenPolicy(); } public function hasAutomaticCapability( $capability, PhabricatorUser $viewer) { return false; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new DiffusionRepositoryIdentityEditor(); } public function getApplicationTransactionTemplate() { return new PhabricatorRepositoryIdentityTransaction(); } }