diff --git a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php index 583e45d5d1..bfe4ef4d40 100644 --- a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php +++ b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php @@ -1,1141 +1,1121 @@ viewer = $viewer; return $this; } protected function requireViewer() { if (!$this->viewer) { throw new PhutilInvalidStateException('setViewer'); } return $this->viewer; } public function setContext($context) { $this->context = $context; return $this; } public function isPanelContext() { return ($this->context == self::CONTEXT_PANEL); } public function canUseInPanelContext() { return true; } public function saveQuery(PhabricatorSavedQuery $query) { $query->setEngineClassName(get_class($this)); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); try { $query->save(); } catch (AphrontDuplicateKeyQueryException $ex) { // Ignore, this is just a repeated search. } unset($unguarded); } /** * Create a saved query object from the request. * * @param AphrontRequest The search request. * @return PhabricatorSavedQuery */ public function buildSavedQueryFromRequest(AphrontRequest $request) { $fields = $this->buildSearchFields(); $viewer = $this->requireViewer(); $saved = new PhabricatorSavedQuery(); foreach ($fields as $field) { $field->setViewer($viewer); $value = $field->readValueFromRequest($request); $saved->setParameter($field->getKey(), $value); } return $saved; } /** * Executes the saved query. * * @param PhabricatorSavedQuery The saved query to operate on. * @return The result of the query. */ public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { $fields = $this->buildSearchFields(); $viewer = $this->requireViewer(); $parameters = array(); foreach ($fields as $field) { $field->setViewer($viewer); $field->readValueFromSavedQuery($saved); $value = $field->getValueForQuery($field->getValue()); $parameters[$field->getKey()] = $value; } $query = $this->buildQueryFromParameters($parameters); $object = $this->newResultObject(); if (!$object) { return $query; } if ($object instanceof PhabricatorProjectInterface) { if (!empty($parameters['projectPHIDs'])) { $query->withEdgeLogicConstraints( PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, $parameters['projectPHIDs']); } } if ($object instanceof PhabricatorSpacesInterface) { if (!empty($parameters['spacePHIDs'])) { $query->withSpacePHIDs($parameters['spacePHIDs']); } } return $query; } protected function buildQueryFromParameters(array $parameters) { throw new PhutilMethodNotImplementedException(); } /** * Builds the search form using the request. * * @param AphrontFormView Form to populate. * @param PhabricatorSavedQuery The query from which to build the form. * @return void */ public function buildSearchForm( AphrontFormView $form, PhabricatorSavedQuery $saved) { $fields = $this->buildSearchFields(); $viewer = $this->requireViewer(); foreach ($fields as $field) { $field->setViewer($viewer); $field->readValueFromSavedQuery($saved); } foreach ($fields as $field) { foreach ($field->getErrors() as $error) { $this->addError(last($error)); } } foreach ($fields as $field) { $field->appendToForm($form); } } protected function buildSearchFields() { $fields = array(); foreach ($this->buildCustomSearchFields() as $field) { $fields[] = $field; } $object = $this->newResultObject(); if (!$object) { return $fields; } if ($object instanceof PhabricatorProjectInterface) { $fields[] = id(new PhabricatorSearchProjectsField()) ->setKey('projectPHIDs') ->setAliases(array('project', 'projects')) ->setLabel(pht('Projects')); } if ($object instanceof PhabricatorSpacesInterface) { if (PhabricatorSpacesNamespaceQuery::getSpacesExist()) { $fields[] = id(new PhabricatorSearchSpacesField()) ->setKey('spacePHIDs') ->setAliases(array('space', 'spaces')) ->setLabel(pht('Spaces')); } } return $fields; } protected function buildCustomSearchFields() { throw new PhutilMethodNotImplementedException(); } public function getErrors() { return $this->errors; } public function addError($error) { $this->errors[] = $error; return $this; } /** * Return an application URI corresponding to the results page of a query. * Normally, this is something like `/application/query/QUERYKEY/`. * * @param string The query key to build a URI for. * @return string URI where the query can be executed. * @task uri */ public function getQueryResultsPageURI($query_key) { return $this->getURI('query/'.$query_key.'/'); } /** * Return an application URI for query management. This is used when, e.g., * a query deletion operation is cancelled. * * @return string URI where queries can be managed. * @task uri */ public function getQueryManagementURI() { return $this->getURI('query/edit/'); } /** * Return the URI to a path within the application. Used to construct default * URIs for management and results. * * @return string URI to path. * @task uri */ abstract protected function getURI($path); /** * Return a human readable description of the type of objects this query * searches for. * * For example, "Tasks" or "Commits". * * @return string Human-readable description of what this engine is used to * find. */ abstract public function getResultTypeDescription(); public function newSavedQuery() { return id(new PhabricatorSavedQuery()) ->setEngineClassName(get_class($this)); } public function addNavigationItems(PHUIListView $menu) { $viewer = $this->requireViewer(); $menu->newLabel(pht('Queries')); $named_queries = $this->loadEnabledNamedQueries(); foreach ($named_queries as $query) { $key = $query->getQueryKey(); $uri = $this->getQueryResultsPageURI($key); $menu->newLink($query->getQueryName(), $uri, 'query/'.$key); } if ($viewer->isLoggedIn()) { $manage_uri = $this->getQueryManagementURI(); $menu->newLink(pht('Edit Queries...'), $manage_uri, 'query/edit'); } $menu->newLabel(pht('Search')); $advanced_uri = $this->getQueryResultsPageURI('advanced'); $menu->newLink(pht('Advanced Search'), $advanced_uri, 'query/advanced'); return $this; } public function loadAllNamedQueries() { $viewer = $this->requireViewer(); $named_queries = id(new PhabricatorNamedQueryQuery()) ->setViewer($viewer) ->withUserPHIDs(array($viewer->getPHID())) ->withEngineClassNames(array(get_class($this))) ->execute(); $named_queries = mpull($named_queries, null, 'getQueryKey'); $builtin = $this->getBuiltinQueries($viewer); $builtin = mpull($builtin, null, 'getQueryKey'); foreach ($named_queries as $key => $named_query) { if ($named_query->getIsBuiltin()) { if (isset($builtin[$key])) { $named_queries[$key]->setQueryName($builtin[$key]->getQueryName()); unset($builtin[$key]); } else { unset($named_queries[$key]); } } unset($builtin[$key]); } $named_queries = msort($named_queries, 'getSortKey'); return $named_queries + $builtin; } public function loadEnabledNamedQueries() { $named_queries = $this->loadAllNamedQueries(); foreach ($named_queries as $key => $named_query) { if ($named_query->getIsBuiltin() && $named_query->getIsDisabled()) { unset($named_queries[$key]); } } return $named_queries; } protected function setQueryProjects( PhabricatorCursorPagedPolicyAwareQuery $query, PhabricatorSavedQuery $saved) { $datasource = id(new PhabricatorProjectLogicalDatasource()) ->setViewer($this->requireViewer()); $projects = $saved->getParameter('projects', array()); $constraints = $datasource->evaluateTokens($projects); if ($constraints) { $query->withEdgeLogicConstraints( PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, $constraints); } } /* -( Applications )------------------------------------------------------- */ protected function getApplicationURI($path = '') { return $this->getApplication()->getApplicationURI($path); } protected function getApplication() { if (!$this->application) { $class = $this->getApplicationClassName(); $this->application = id(new PhabricatorApplicationQuery()) ->setViewer($this->requireViewer()) ->withClasses(array($class)) ->withInstalled(true) ->executeOne(); if (!$this->application) { throw new Exception( pht( 'Application "%s" is not installed!', $class)); } } return $this->application; } abstract public function getApplicationClassName(); /* -( Constructing Engines )----------------------------------------------- */ /** * Load all available application search engines. * * @return list All available engines. * @task construct */ public static function getAllEngines() { $engines = id(new PhutilSymbolLoader()) ->setAncestorClass(__CLASS__) ->loadObjects(); return $engines; } /** * Get an engine by class name, if it exists. * * @return PhabricatorApplicationSearchEngine|null Engine, or null if it does * not exist. * @task construct */ public static function getEngineByClassName($class_name) { return idx(self::getAllEngines(), $class_name); } /* -( Builtin Queries )---------------------------------------------------- */ /** * @task builtin */ public function getBuiltinQueries() { $names = $this->getBuiltinQueryNames(); $queries = array(); $sequence = 0; foreach ($names as $key => $name) { $queries[$key] = id(new PhabricatorNamedQuery()) ->setUserPHID($this->requireViewer()->getPHID()) ->setEngineClassName(get_class($this)) ->setQueryName($name) ->setQueryKey($key) ->setSequence((1 << 24) + $sequence++) ->setIsBuiltin(true); } return $queries; } /** * @task builtin */ public function getBuiltinQuery($query_key) { if (!$this->isBuiltinQuery($query_key)) { throw new Exception(pht("'%s' is not a builtin!", $query_key)); } return idx($this->getBuiltinQueries(), $query_key); } /** * @task builtin */ protected function getBuiltinQueryNames() { return array(); } /** * @task builtin */ public function isBuiltinQuery($query_key) { $builtins = $this->getBuiltinQueries(); return isset($builtins[$query_key]); } /** * @task builtin */ public function buildSavedQueryFromBuiltin($query_key) { throw new Exception(pht("Builtin '%s' is not supported!", $query_key)); } /* -( Reading Utilities )--------------------------------------------------- */ /** * Read a list of user PHIDs from a request in a flexible way. This method * supports either of these forms: * * users[]=alincoln&users[]=htaft * users=alincoln,htaft * * Additionally, users can be specified either by PHID or by name. * * The main goal of this flexibility is to allow external programs to generate * links to pages (like "alincoln's open revisions") without needing to make * API calls. * * @param AphrontRequest Request to read user PHIDs from. * @param string Key to read in the request. * @param list Other permitted PHID types. * @return list List of user PHIDs and selector functions. * @task read */ protected function readUsersFromRequest( AphrontRequest $request, $key, array $allow_types = array()) { $list = $this->readListFromRequest($request, $key); $phids = array(); $names = array(); $allow_types = array_fuse($allow_types); $user_type = PhabricatorPeopleUserPHIDType::TYPECONST; foreach ($list as $item) { $type = phid_get_type($item); if ($type == $user_type) { $phids[] = $item; } else if (isset($allow_types[$type])) { $phids[] = $item; } else { if (PhabricatorTypeaheadDatasource::isFunctionToken($item)) { // If this is a function, pass it through unchanged; we'll evaluate // it later. $phids[] = $item; } else { $names[] = $item; } } } if ($names) { $users = id(new PhabricatorPeopleQuery()) ->setViewer($this->requireViewer()) ->withUsernames($names) ->execute(); foreach ($users as $user) { $phids[] = $user->getPHID(); } $phids = array_unique($phids); } return $phids; } /** * Read a list of project PHIDs from a request in a flexible way. * * @param AphrontRequest Request to read user PHIDs from. * @param string Key to read in the request. * @return list List of projet PHIDs and selector functions. * @task read */ protected function readProjectsFromRequest(AphrontRequest $request, $key) { $list = $this->readListFromRequest($request, $key); $phids = array(); $slugs = array(); $project_type = PhabricatorProjectProjectPHIDType::TYPECONST; foreach ($list as $item) { $type = phid_get_type($item); if ($type == $project_type) { $phids[] = $item; } else { if (PhabricatorTypeaheadDatasource::isFunctionToken($item)) { // If this is a function, pass it through unchanged; we'll evaluate // it later. $phids[] = $item; } else { $slugs[] = $item; } } } if ($slugs) { $projects = id(new PhabricatorProjectQuery()) ->setViewer($this->requireViewer()) ->withSlugs($slugs) ->execute(); foreach ($projects as $project) { $phids[] = $project->getPHID(); } $phids = array_unique($phids); } return $phids; } /** * Read a list of subscribers from a request in a flexible way. * * @param AphrontRequest Request to read PHIDs from. * @param string Key to read in the request. * @return list List of object PHIDs. * @task read */ protected function readSubscribersFromRequest( AphrontRequest $request, $key) { return $this->readUsersFromRequest( $request, $key, array( PhabricatorProjectProjectPHIDType::TYPECONST, )); } /** * Read a list of generic PHIDs from a request in a flexible way. Like * @{method:readUsersFromRequest}, this method supports either array or * comma-delimited forms. Objects can be specified either by PHID or by * object name. * * @param AphrontRequest Request to read PHIDs from. * @param string Key to read in the request. * @param list Optional, list of permitted PHID types. * @return list List of object PHIDs. * * @task read */ protected function readPHIDsFromRequest( AphrontRequest $request, $key, array $allow_types = array()) { $list = $this->readListFromRequest($request, $key); $objects = id(new PhabricatorObjectQuery()) ->setViewer($this->requireViewer()) ->withNames($list) ->execute(); $list = mpull($objects, 'getPHID'); if (!$list) { return array(); } // If only certain PHID types are allowed, filter out all the others. if ($allow_types) { $allow_types = array_fuse($allow_types); foreach ($list as $key => $phid) { if (empty($allow_types[phid_get_type($phid)])) { unset($list[$key]); } } } return $list; } /** * Read a list of items from the request, in either array format or string * format: * * list[]=item1&list[]=item2 * list=item1,item2 * * This provides flexibility when constructing URIs, especially from external * sources. * * @param AphrontRequest Request to read strings from. * @param string Key to read in the request. * @return list List of values. */ protected function readListFromRequest( AphrontRequest $request, $key) { $list = $request->getArr($key, null); if ($list === null) { $list = $request->getStrList($key); } if (!$list) { return array(); } return $list; } protected function readDateFromRequest( AphrontRequest $request, $key) { $value = AphrontFormDateControlValue::newFromRequest($request, $key); if ($value->isEmpty()) { return null; } return $value->getDictionary(); } protected function readBoolFromRequest( AphrontRequest $request, $key) { if (!strlen($request->getStr($key))) { return null; } return $request->getBool($key); } protected function getBoolFromQuery(PhabricatorSavedQuery $query, $key) { $value = $query->getParameter($key); if ($value === null) { return $value; } return $value ? 'true' : 'false'; } /* -( Dates )-------------------------------------------------------------- */ /** * @task dates */ protected function parseDateTime($date_time) { if (!strlen($date_time)) { return null; } return PhabricatorTime::parseLocalTime($date_time, $this->requireViewer()); } /** * @task dates */ protected function buildDateRange( AphrontFormView $form, PhabricatorSavedQuery $saved_query, $start_key, $start_name, $end_key, $end_name) { $start_str = $saved_query->getParameter($start_key); $start = null; if (strlen($start_str)) { $start = $this->parseDateTime($start_str); if (!$start) { $this->addError( pht( '"%s" date can not be parsed.', $start_name)); } } $end_str = $saved_query->getParameter($end_key); $end = null; if (strlen($end_str)) { $end = $this->parseDateTime($end_str); if (!$end) { $this->addError( pht( '"%s" date can not be parsed.', $end_name)); } } if ($start && $end && ($start >= $end)) { $this->addError( pht( '"%s" must be a date before "%s".', $start_name, $end_name)); } $form ->appendChild( id(new PHUIFormFreeformDateControl()) ->setName($start_key) ->setLabel($start_name) ->setValue($start_str)) ->appendChild( id(new AphrontFormTextControl()) ->setName($end_key) ->setLabel($end_name) ->setValue($end_str)); } /* -( Result Ordering )---------------------------------------------------- */ /** * Save order selection to a @{class:PhabricatorSavedQuery}. */ protected function saveQueryOrder( PhabricatorSavedQuery $saved, AphrontRequest $request) { $saved->setParameter('order', $request->getStr('order')); return $this; } /** * Set query ordering from a saved value. */ protected function setQueryOrder( PhabricatorCursorPagedPolicyAwareQuery $query, PhabricatorSavedQuery $saved) { $order = $saved->getParameter('order'); $builtin = $query->getBuiltinOrders(); if (strlen($order) && isset($builtin[$order])) { $query->setOrder($order); } else { // If the order is invalid or not available, we choose the first // builtin order. This isn't always the default order for the query, // but is the first value in the "Order" dropdown, and makes the query // behavior more consistent with the UI. In queries where the two // orders differ, this order is the preferred order for humans. $query->setOrder(head_key($builtin)); } return $this; } protected function appendOrderFieldsToForm( AphrontFormView $form, PhabricatorSavedQuery $saved, PhabricatorCursorPagedPolicyAwareQuery $query) { $orders = $query->getBuiltinOrders(); $orders = ipull($orders, 'name'); $form->appendControl( id(new AphrontFormSelectControl()) ->setLabel(pht('Order')) ->setName('order') ->setOptions($orders) ->setValue($saved->getParameter('order'))); } /* -( Paging and Executing Queries )--------------------------------------- */ public function getPageSize(PhabricatorSavedQuery $saved) { return $saved->getParameter('limit', 100); } public function shouldUseOffsetPaging() { return false; } public function newPagerForSavedQuery(PhabricatorSavedQuery $saved) { if ($this->shouldUseOffsetPaging()) { $pager = new AphrontPagerView(); } else { $pager = new AphrontCursorPagerView(); } $page_size = $this->getPageSize($saved); if (is_finite($page_size)) { $pager->setPageSize($page_size); } else { // Consider an INF pagesize to mean a large finite pagesize. // TODO: It would be nice to handle this more gracefully, but math // with INF seems to vary across PHP versions, systems, and runtimes. $pager->setPageSize(0xFFFF); } return $pager; } public function executeQuery( PhabricatorPolicyAwareQuery $query, AphrontView $pager) { $query->setViewer($this->requireViewer()); if ($this->shouldUseOffsetPaging()) { $objects = $query->executeWithOffsetPager($pager); } else { $objects = $query->executeWithCursorPager($pager); } return $objects; } /* -( Rendering )---------------------------------------------------------- */ public function setRequest(AphrontRequest $request) { $this->request = $request; return $this; } public function getRequest() { return $this->request; } public function renderResults( array $objects, PhabricatorSavedQuery $query) { $phids = $this->getRequiredHandlePHIDsForResultList($objects, $query); if ($phids) { $handles = id(new PhabricatorHandleQuery()) ->setViewer($this->requireViewer()) ->witHPHIDs($phids) ->execute(); } else { $handles = array(); } return $this->renderResultList($objects, $query, $handles); } protected function getRequiredHandlePHIDsForResultList( array $objects, PhabricatorSavedQuery $query) { return array(); } protected function renderResultList( array $objects, PhabricatorSavedQuery $query, array $handles) { throw new Exception(pht('Not supported here yet!')); } /* -( Application Search )------------------------------------------------- */ /** * Retrieve an object to use to define custom fields for this search. * * To integrate with custom fields, subclasses should override this method * and return an instance of the application object which implements * @{interface:PhabricatorCustomFieldInterface}. * * @return PhabricatorCustomFieldInterface|null Object with custom fields. * @task appsearch */ public function getCustomFieldObject() { $object = $this->newResultObject(); if ($object instanceof PhabricatorCustomFieldInterface) { return $object; } return null; } /** * Get the custom fields for this search. * * @return PhabricatorCustomFieldList|null Custom fields, if this search * supports custom fields. * @task appsearch */ public function getCustomFieldList() { if ($this->customFields === false) { $object = $this->getCustomFieldObject(); if ($object) { $fields = PhabricatorCustomField::getObjectFields( $object, PhabricatorCustomField::ROLE_APPLICATIONSEARCH); $fields->setViewer($this->requireViewer()); } else { $fields = null; } $this->customFields = $fields; } return $this->customFields; } /** * Moves data from the request into a saved query. * * @param AphrontRequest Request to read. * @param PhabricatorSavedQuery Query to write to. * @return void * @task appsearch */ protected function readCustomFieldsFromRequest( AphrontRequest $request, PhabricatorSavedQuery $saved) { $list = $this->getCustomFieldList(); if (!$list) { return; } foreach ($list->getFields() as $field) { $key = $this->getKeyForCustomField($field); $value = $field->readApplicationSearchValueFromRequest( $this, $request); $saved->setParameter($key, $value); } } /** * Applies data from a saved query to an executable query. * * @param PhabricatorCursorPagedPolicyAwareQuery Query to constrain. * @param PhabricatorSavedQuery Saved query to read. * @return void */ protected function applyCustomFieldsToQuery( PhabricatorCursorPagedPolicyAwareQuery $query, PhabricatorSavedQuery $saved) { $list = $this->getCustomFieldList(); if (!$list) { return; } foreach ($list->getFields() as $field) { $key = $this->getKeyForCustomField($field); $value = $field->applyApplicationSearchConstraintToQuery( $this, $query, $saved->getParameter($key)); } } protected function applyOrderByToQuery( PhabricatorCursorPagedPolicyAwareQuery $query, array $standard_values, $order) { if (substr($order, 0, 7) === 'custom:') { $list = $this->getCustomFieldList(); if (!$list) { $query->setOrderBy(head($standard_values)); return; } foreach ($list->getFields() as $field) { $key = $this->getKeyForCustomField($field); if ($key === $order) { $index = $field->buildOrderIndex(); if ($index === null) { $query->setOrderBy(head($standard_values)); return; } $query->withApplicationSearchOrder( $field, $index, false); break; } } } else { $order = idx($standard_values, $order); if ($order) { $query->setOrderBy($order); } else { $query->setOrderBy(head($standard_values)); } } } protected function getCustomFieldOrderOptions() { $list = $this->getCustomFieldList(); if (!$list) { return; } $custom_order = array(); foreach ($list->getFields() as $field) { if ($field->shouldAppearInApplicationSearch()) { if ($field->buildOrderIndex() !== null) { $key = $this->getKeyForCustomField($field); $custom_order[$key] = $field->getFieldName(); } } } return $custom_order; } /** * Get a unique key identifying a field. * * @param PhabricatorCustomField Field to identify. * @return string Unique identifier, suitable for use as an input name. */ public function getKeyForCustomField(PhabricatorCustomField $field) { return 'custom:'.$field->getFieldIndex(); } /** * Add inputs to an application search form so the user can query on custom * fields. * * @param AphrontFormView Form to update. * @param PhabricatorSavedQuery Values to prefill. * @return void */ protected function appendCustomFieldsToForm( AphrontFormView $form, PhabricatorSavedQuery $saved) { $list = $this->getCustomFieldList(); if (!$list) { return; } - $phids = array(); - foreach ($list->getFields() as $field) { - $key = $this->getKeyForCustomField($field); - $value = $saved->getParameter($key); - $phids[$key] = $field->getRequiredHandlePHIDsForApplicationSearch($value); - } - $all_phids = array_mergev($phids); - - $handles = array(); - if ($all_phids) { - $handles = id(new PhabricatorHandleQuery()) - ->setViewer($this->requireViewer()) - ->withPHIDs($all_phids) - ->execute(); - } - foreach ($list->getFields() as $field) { $key = $this->getKeyForCustomField($field); $value = $saved->getParameter($key); - $field->appendToApplicationSearchForm( - $this, - $form, - $value, - array_select_keys($handles, $phids[$key])); + $field->appendToApplicationSearchForm($this, $form, $value); } } } diff --git a/src/infrastructure/customfield/field/PhabricatorCustomField.php b/src/infrastructure/customfield/field/PhabricatorCustomField.php index b154e0e065..18c112516b 100644 --- a/src/infrastructure/customfield/field/PhabricatorCustomField.php +++ b/src/infrastructure/customfield/field/PhabricatorCustomField.php @@ -1,1379 +1,1358 @@ getCustomFields(); } catch (PhabricatorDataNotAttachedException $ex) { $attachment = new PhabricatorCustomFieldAttachment(); $object->attachCustomFields($attachment); } try { $field_list = $attachment->getCustomFieldList($role); } catch (PhabricatorCustomFieldNotAttachedException $ex) { $base_class = $object->getCustomFieldBaseClass(); $spec = $object->getCustomFieldSpecificationForRole($role); if (!is_array($spec)) { $obj_class = get_class($object); throw new Exception( "Expected an array from getCustomFieldSpecificationForRole() for ". "object of class '{$obj_class}'."); } $fields = self::buildFieldList( $base_class, $spec, $object); foreach ($fields as $key => $field) { if (!$field->shouldEnableForRole($role)) { unset($fields[$key]); } } foreach ($fields as $field) { $field->setObject($object); } $field_list = new PhabricatorCustomFieldList($fields); $attachment->addCustomFieldList($role, $field_list); } return $field_list; } /** * @task apps */ public static function getObjectField( PhabricatorCustomFieldInterface $object, $role, $field_key) { $fields = self::getObjectFields($object, $role)->getFields(); return idx($fields, $field_key); } /** * @task apps */ public static function buildFieldList( $base_class, array $spec, $object, array $options = array()) { PhutilTypeSpec::checkMap( $options, array( 'withDisabled' => 'optional bool', )); $field_objects = id(new PhutilSymbolLoader()) ->setAncestorClass($base_class) ->loadObjects(); $fields = array(); $from_map = array(); foreach ($field_objects as $field_object) { $current_class = get_class($field_object); foreach ($field_object->createFields($object) as $field) { $key = $field->getFieldKey(); if (isset($fields[$key])) { $original_class = $from_map[$key]; throw new Exception( "Both '{$original_class}' and '{$current_class}' define a custom ". "field with field key '{$key}'. Field keys must be unique."); } $from_map[$key] = $current_class; $fields[$key] = $field; } } foreach ($fields as $key => $field) { if (!$field->isFieldEnabled()) { unset($fields[$key]); } } $fields = array_select_keys($fields, array_keys($spec)) + $fields; if (empty($options['withDisabled'])) { foreach ($fields as $key => $field) { $config = idx($spec, $key, array()) + array( 'disabled' => $field->shouldDisableByDefault(), ); if (!empty($config['disabled'])) { if ($field->canDisableField()) { unset($fields[$key]); } } } } return $fields; } /* -( Core Properties and Field Identity )--------------------------------- */ /** * Return a key which uniquely identifies this field, like * "mycompany:dinosaur:count". Normally you should provide some level of * namespacing to prevent collisions. * * @return string String which uniquely identifies this field. * @task core */ public function getFieldKey() { if ($this->proxy) { return $this->proxy->getFieldKey(); } throw new PhabricatorCustomFieldImplementationIncompleteException( $this, $field_key_is_incomplete = true); } /** * Return a human-readable field name. * * @return string Human readable field name. * @task core */ public function getFieldName() { if ($this->proxy) { return $this->proxy->getFieldName(); } return $this->getFieldKey(); } /** * Return a short, human-readable description of the field's behavior. This * provides more context to administrators when they are customizing fields. * * @return string|null Optional human-readable description. * @task core */ public function getFieldDescription() { if ($this->proxy) { return $this->proxy->getFieldDescription(); } return null; } /** * Most field implementations are unique, in that one class corresponds to * one field. However, some field implementations are general and a single * implementation may drive several fields. * * For general implementations, the general field implementation can return * multiple field instances here. * * @param object The object to create fields for. * @return list List of fields. * @task core */ public function createFields($object) { return array($this); } /** * You can return `false` here if the field should not be enabled for any * role. For example, it might depend on something (like an application or * library) which isn't installed, or might have some global configuration * which allows it to be disabled. * * @return bool False to completely disable this field for all roles. * @task core */ public function isFieldEnabled() { if ($this->proxy) { return $this->proxy->isFieldEnabled(); } return true; } /** * Low level selector for field availability. Fields can appear in different * roles (like an edit view, a list view, etc.), but not every field needs * to appear everywhere. Fields that are disabled in a role won't appear in * that context within applications. * * Normally, you do not need to override this method. Instead, override the * methods specific to roles you want to enable. For example, implement * @{method:shouldUseStorage()} to activate the `'storage'` role. * * @return bool True to enable the field for the given role. * @task core */ public function shouldEnableForRole($role) { // NOTE: All of these calls proxy individually, so we don't need to // proxy this call as a whole. switch ($role) { case self::ROLE_APPLICATIONTRANSACTIONS: return $this->shouldAppearInApplicationTransactions(); case self::ROLE_APPLICATIONSEARCH: return $this->shouldAppearInApplicationSearch(); case self::ROLE_STORAGE: return $this->shouldUseStorage(); case self::ROLE_EDIT: return $this->shouldAppearInEditView(); case self::ROLE_VIEW: return $this->shouldAppearInPropertyView(); case self::ROLE_LIST: return $this->shouldAppearInListView(); case self::ROLE_GLOBALSEARCH: return $this->shouldAppearInGlobalSearch(); case self::ROLE_CONDUIT: return $this->shouldAppearInConduitDictionary(); case self::ROLE_TRANSACTIONMAIL: return $this->shouldAppearInTransactionMail(); case self::ROLE_HERALD: return $this->shouldAppearInHerald(); case self::ROLE_DEFAULT: return true; default: throw new Exception("Unknown field role '{$role}'!"); } } /** * Allow administrators to disable this field. Most fields should allow this, * but some are fundamental to the behavior of the application and can be * locked down to avoid chaos, disorder, and the decline of civilization. * * @return bool False to prevent this field from being disabled through * configuration. * @task core */ public function canDisableField() { return true; } public function shouldDisableByDefault() { return false; } /** * Return an index string which uniquely identifies this field. * * @return string Index string which uniquely identifies this field. * @task core */ final public function getFieldIndex() { return PhabricatorHash::digestForIndex($this->getFieldKey()); } /* -( Field Proxies )------------------------------------------------------ */ /** * Proxies allow a field to use some other field's implementation for most * of their behavior while still subclassing an application field. When a * proxy is set for a field with @{method:setProxy}, all of its methods will * call through to the proxy by default. * * This is most commonly used to implement configuration-driven custom fields * using @{class:PhabricatorStandardCustomField}. * * This method must be overridden to return `true` before a field can accept * proxies. * * @return bool True if you can @{method:setProxy} this field. * @task proxy */ public function canSetProxy() { if ($this instanceof PhabricatorStandardCustomFieldInterface) { return true; } return false; } /** * Set the proxy implementation for this field. See @{method:canSetProxy} for * discussion of field proxies. * * @param PhabricatorCustomField Field implementation. * @return this */ final public function setProxy(PhabricatorCustomField $proxy) { if (!$this->canSetProxy()) { throw new PhabricatorCustomFieldNotProxyException($this); } $this->proxy = $proxy; return $this; } /** * Get the field's proxy implementation, if any. For discussion, see * @{method:canSetProxy}. * * @return PhabricatorCustomField|null Proxy field, if one is set. */ final public function getProxy() { return $this->proxy; } /* -( Contextual Data )---------------------------------------------------- */ /** * Sets the object this field belongs to. * * @param PhabricatorCustomFieldInterface The object this field belongs to. * @return this * @task context */ final public function setObject(PhabricatorCustomFieldInterface $object) { if ($this->proxy) { $this->proxy->setObject($object); return $this; } $this->object = $object; $this->didSetObject($object); return $this; } /** * Read object data into local field storage, if applicable. * * @param PhabricatorCustomFieldInterface The object this field belongs to. * @return this * @task context */ public function readValueFromObject(PhabricatorCustomFieldInterface $object) { if ($this->proxy) { $this->proxy->readValueFromObject($object); } return $this; } /** * Get the object this field belongs to. * * @return PhabricatorCustomFieldInterface The object this field belongs to. * @task context */ final public function getObject() { if ($this->proxy) { return $this->proxy->getObject(); } return $this->object; } /** * This is a hook, primarily for subclasses to load object data. * * @return PhabricatorCustomFieldInterface The object this field belongs to. * @return void */ protected function didSetObject(PhabricatorCustomFieldInterface $object) { return; } /** * @task context */ final public function setViewer(PhabricatorUser $viewer) { if ($this->proxy) { $this->proxy->setViewer($viewer); return $this; } $this->viewer = $viewer; return $this; } /** * @task context */ final public function getViewer() { if ($this->proxy) { return $this->proxy->getViewer(); } return $this->viewer; } /** * @task context */ final protected function requireViewer() { if ($this->proxy) { return $this->proxy->requireViewer(); } if (!$this->viewer) { throw new PhabricatorCustomFieldDataNotAvailableException($this); } return $this->viewer; } /* -( Rendering Utilities )------------------------------------------------ */ /** * @task render */ protected function renderHandleList(array $handles) { if (!$handles) { return null; } $out = array(); foreach ($handles as $handle) { $out[] = $handle->renderLink(); } return phutil_implode_html(phutil_tag('br'), $out); } /* -( Storage )------------------------------------------------------------ */ /** * Return true to use field storage. * * Fields which can be edited by the user will most commonly use storage, * while some other types of fields (for instance, those which just display * information in some stylized way) may not. Many builtin fields do not use * storage because their data is available on the object itself. * * If you implement this, you must also implement @{method:getValueForStorage} * and @{method:setValueFromStorage}. * * @return bool True to use storage. * @task storage */ public function shouldUseStorage() { if ($this->proxy) { return $this->proxy->shouldUseStorage(); } return false; } /** * Return a new, empty storage object. This should be a subclass of * @{class:PhabricatorCustomFieldStorage} which is bound to the application's * database. * * @return PhabricatorCustomFieldStorage New empty storage object. * @task storage */ public function newStorageObject() { if ($this->proxy) { return $this->proxy->newStorageObject(); } throw new PhabricatorCustomFieldImplementationIncompleteException($this); } /** * Return a serialized representation of the field value, appropriate for * storing in auxiliary field storage. You must implement this method if * you implement @{method:shouldUseStorage}. * * If the field value is a scalar, it can be returned unmodiifed. If not, * it should be serialized (for example, using JSON). * * @return string Serialized field value. * @task storage */ public function getValueForStorage() { if ($this->proxy) { return $this->proxy->getValueForStorage(); } throw new PhabricatorCustomFieldImplementationIncompleteException($this); } /** * Set the field's value given a serialized storage value. This is called * when the field is loaded; if no data is available, the value will be * null. You must implement this method if you implement * @{method:shouldUseStorage}. * * Usually, the value can be loaded directly. If it isn't a scalar, you'll * need to undo whatever serialization you applied in * @{method:getValueForStorage}. * * @param string|null Serialized field representation (from * @{method:getValueForStorage}) or null if no value has * ever been stored. * @return this * @task storage */ public function setValueFromStorage($value) { if ($this->proxy) { return $this->proxy->setValueFromStorage($value); } throw new PhabricatorCustomFieldImplementationIncompleteException($this); } /* -( ApplicationSearch )-------------------------------------------------- */ /** * Appearing in ApplicationSearch allows a field to be indexed and searched * for. * * @return bool True to appear in ApplicationSearch. * @task appsearch */ public function shouldAppearInApplicationSearch() { if ($this->proxy) { return $this->proxy->shouldAppearInApplicationSearch(); } return false; } /** * Return one or more indexes which this field can meaningfully query against * to implement ApplicationSearch. * * Normally, you should build these using @{method:newStringIndex} and * @{method:newNumericIndex}. For example, if a field holds a numeric value * it might return a single numeric index: * * return array($this->newNumericIndex($this->getValue())); * * If a field holds a more complex value (like a list of users), it might * return several string indexes: * * $indexes = array(); * foreach ($this->getValue() as $phid) { * $indexes[] = $this->newStringIndex($phid); * } * return $indexes; * * @return list List of indexes. * @task appsearch */ public function buildFieldIndexes() { if ($this->proxy) { return $this->proxy->buildFieldIndexes(); } return array(); } /** * Return an index against which this field can be meaningfully ordered * against to implement ApplicationSearch. * * This should be a single index, normally built using * @{method:newStringIndex} and @{method:newNumericIndex}. * * The value of the index is not used. * * Return null from this method if the field can not be ordered. * * @return PhabricatorCustomFieldIndexStorage A single index to order by. * @task appsearch */ public function buildOrderIndex() { if ($this->proxy) { return $this->proxy->buildOrderIndex(); } return null; } /** * Build a new empty storage object for storing string indexes. Normally, * this should be a concrete subclass of * @{class:PhabricatorCustomFieldStringIndexStorage}. * * @return PhabricatorCustomFieldStringIndexStorage Storage object. * @task appsearch */ protected function newStringIndexStorage() { // NOTE: This intentionally isn't proxied, to avoid call cycles. throw new PhabricatorCustomFieldImplementationIncompleteException($this); } /** * Build a new empty storage object for storing string indexes. Normally, * this should be a concrete subclass of * @{class:PhabricatorCustomFieldStringIndexStorage}. * * @return PhabricatorCustomFieldStringIndexStorage Storage object. * @task appsearch */ protected function newNumericIndexStorage() { // NOTE: This intentionally isn't proxied, to avoid call cycles. throw new PhabricatorCustomFieldImplementationIncompleteException($this); } /** * Build and populate storage for a string index. * * @param string String to index. * @return PhabricatorCustomFieldStringIndexStorage Populated storage. * @task appsearch */ protected function newStringIndex($value) { if ($this->proxy) { return $this->proxy->newStringIndex(); } $key = $this->getFieldIndex(); return $this->newStringIndexStorage() ->setIndexKey($key) ->setIndexValue($value); } /** * Build and populate storage for a numeric index. * * @param string Numeric value to index. * @return PhabricatorCustomFieldNumericIndexStorage Populated storage. * @task appsearch */ protected function newNumericIndex($value) { if ($this->proxy) { return $this->proxy->newNumericIndex(); } $key = $this->getFieldIndex(); return $this->newNumericIndexStorage() ->setIndexKey($key) ->setIndexValue($value); } /** * Read a query value from a request, for storage in a saved query. Normally, * this method should, e.g., read a string out of the request. * * @param PhabricatorApplicationSearchEngine Engine building the query. * @param AphrontRequest Request to read from. * @return wild * @task appsearch */ public function readApplicationSearchValueFromRequest( PhabricatorApplicationSearchEngine $engine, AphrontRequest $request) { if ($this->proxy) { return $this->proxy->readApplicationSearchValueFromRequest( $engine, $request); } throw new PhabricatorCustomFieldImplementationIncompleteException($this); } /** * Constrain a query, given a field value. Generally, this method should * use `with...()` methods to apply filters or other constraints to the * query. * * @param PhabricatorApplicationSearchEngine Engine executing the query. * @param PhabricatorCursorPagedPolicyAwareQuery Query to constrain. * @param wild Constraint provided by the user. * @return void * @task appsearch */ public function applyApplicationSearchConstraintToQuery( PhabricatorApplicationSearchEngine $engine, PhabricatorCursorPagedPolicyAwareQuery $query, $value) { if ($this->proxy) { return $this->proxy->applyApplicationSearchConstraintToQuery( $engine, $query, $value); } throw new PhabricatorCustomFieldImplementationIncompleteException($this); } /** - * Append search controls to the interface. If you need handles, use - * @{method:getRequiredHandlePHIDsForApplicationSearch} to get them. + * Append search controls to the interface. * * @param PhabricatorApplicationSearchEngine Engine constructing the form. * @param AphrontFormView The form to update. * @param wild Value from the saved query. - * @param list List of handles. * @return void * @task appsearch */ public function appendToApplicationSearchForm( PhabricatorApplicationSearchEngine $engine, AphrontFormView $form, - $value, - array $handles) { + $value) { if ($this->proxy) { return $this->proxy->appendToApplicationSearchForm( $engine, $form, - $value, - $handles); + $value); } throw new PhabricatorCustomFieldImplementationIncompleteException($this); } - /** - * Return a list of PHIDs which @{method:appendToApplicationSearchForm} needs - * handles for. This is primarily useful if the field stores PHIDs and you - * need to (for example) render a tokenizer control. - * - * @param wild Value from the saved query. - * @return list List of PHIDs. - * @task appsearch - */ - public function getRequiredHandlePHIDsForApplicationSearch($value) { - if ($this->proxy) { - return $this->proxy->getRequiredHandlePHIDsForApplicationSearch($value); - } - return array(); - } - - /* -( ApplicationTransactions )-------------------------------------------- */ /** * Appearing in ApplicationTrasactions allows a field to be edited using * standard workflows. * * @return bool True to appear in ApplicationTransactions. * @task appxaction */ public function shouldAppearInApplicationTransactions() { if ($this->proxy) { return $this->proxy->shouldAppearInApplicationTransactions(); } return false; } /** * @task appxaction */ public function getApplicationTransactionType() { if ($this->proxy) { return $this->proxy->getApplicationTransactionType(); } return PhabricatorTransactions::TYPE_CUSTOMFIELD; } /** * @task appxaction */ public function getApplicationTransactionMetadata() { if ($this->proxy) { return $this->proxy->getApplicationTransactionMetadata(); } return array(); } /** * @task appxaction */ public function getOldValueForApplicationTransactions() { if ($this->proxy) { return $this->proxy->getOldValueForApplicationTransactions(); } return $this->getValueForStorage(); } /** * @task appxaction */ public function getNewValueForApplicationTransactions() { if ($this->proxy) { return $this->proxy->getNewValueForApplicationTransactions(); } return $this->getValueForStorage(); } /** * @task appxaction */ public function setValueFromApplicationTransactions($value) { if ($this->proxy) { return $this->proxy->setValueFromApplicationTransactions($value); } return $this->setValueFromStorage($value); } /** * @task appxaction */ public function getNewValueFromApplicationTransactions( PhabricatorApplicationTransaction $xaction) { if ($this->proxy) { return $this->proxy->getNewValueFromApplicationTransactions($xaction); } return $xaction->getNewValue(); } /** * @task appxaction */ public function getApplicationTransactionHasEffect( PhabricatorApplicationTransaction $xaction) { if ($this->proxy) { return $this->proxy->getApplicationTransactionHasEffect($xaction); } return ($xaction->getOldValue() !== $xaction->getNewValue()); } /** * @task appxaction */ public function applyApplicationTransactionInternalEffects( PhabricatorApplicationTransaction $xaction) { if ($this->proxy) { return $this->proxy->applyApplicationTransactionInternalEffects($xaction); } return; } /** * @task appxaction */ public function getApplicationTransactionRemarkupBlocks( PhabricatorApplicationTransaction $xaction) { if ($this->proxy) { return $this->proxy->getApplicationTransactionRemarkupBlocks($xaction); } return array(); } /** * @task appxaction */ public function applyApplicationTransactionExternalEffects( PhabricatorApplicationTransaction $xaction) { if ($this->proxy) { return $this->proxy->applyApplicationTransactionExternalEffects($xaction); } if (!$this->shouldEnableForRole(self::ROLE_STORAGE)) { return; } $this->setValueFromApplicationTransactions($xaction->getNewValue()); $value = $this->getValueForStorage(); $table = $this->newStorageObject(); $conn_w = $table->establishConnection('w'); if ($value === null) { queryfx( $conn_w, 'DELETE FROM %T WHERE objectPHID = %s AND fieldIndex = %s', $table->getTableName(), $this->getObject()->getPHID(), $this->getFieldIndex()); } else { queryfx( $conn_w, 'INSERT INTO %T (objectPHID, fieldIndex, fieldValue) VALUES (%s, %s, %s) ON DUPLICATE KEY UPDATE fieldValue = VALUES(fieldValue)', $table->getTableName(), $this->getObject()->getPHID(), $this->getFieldIndex(), $value); } return; } /** * Validate transactions for an object. This allows you to raise an error * when a transaction would set a field to an invalid value, or when a field * is required but no transactions provide value. * * @param PhabricatorLiskDAO Editor applying the transactions. * @param string Transaction type. This type is always * `PhabricatorTransactions::TYPE_CUSTOMFIELD`, it is provided for * convenience when constructing exceptions. * @param list Transactions being applied, * which may be empty if this field is not being edited. * @return list Validation * errors. * * @task appxaction */ public function validateApplicationTransactions( PhabricatorApplicationTransactionEditor $editor, $type, array $xactions) { if ($this->proxy) { return $this->proxy->validateApplicationTransactions( $editor, $type, $xactions); } return array(); } public function getApplicationTransactionTitle( PhabricatorApplicationTransaction $xaction) { if ($this->proxy) { return $this->proxy->getApplicationTransactionTitle( $xaction); } $author_phid = $xaction->getAuthorPHID(); return pht( '%s updated this object.', $xaction->renderHandleLink($author_phid)); } public function getApplicationTransactionTitleForFeed( PhabricatorApplicationTransaction $xaction) { if ($this->proxy) { return $this->proxy->getApplicationTransactionTitleForFeed( $xaction); } $author_phid = $xaction->getAuthorPHID(); $object_phid = $xaction->getObjectPHID(); return pht( '%s updated %s.', $xaction->renderHandleLink($author_phid), $xaction->renderHandleLink($object_phid)); } public function getApplicationTransactionHasChangeDetails( PhabricatorApplicationTransaction $xaction) { if ($this->proxy) { return $this->proxy->getApplicationTransactionHasChangeDetails( $xaction); } return false; } public function getApplicationTransactionChangeDetails( PhabricatorApplicationTransaction $xaction, PhabricatorUser $viewer) { if ($this->proxy) { return $this->proxy->getApplicationTransactionChangeDetails( $xaction, $viewer); } return null; } public function getApplicationTransactionRequiredHandlePHIDs( PhabricatorApplicationTransaction $xaction) { if ($this->proxy) { return $this->proxy->getApplicationTransactionRequiredHandlePHIDs( $xaction); } return array(); } public function shouldHideInApplicationTransactions( PhabricatorApplicationTransaction $xaction) { if ($this->proxy) { return $this->proxy->shouldHideInApplicationTransactions($xaction); } return false; } /* -( Transaction Mail )--------------------------------------------------- */ /** * @task xactionmail */ public function shouldAppearInTransactionMail() { if ($this->proxy) { return $this->proxy->shouldAppearInTransactionMail(); } return false; } /** * @task xactionmail */ public function updateTransactionMailBody( PhabricatorMetaMTAMailBody $body, PhabricatorApplicationTransactionEditor $editor, array $xactions) { if ($this->proxy) { return $this->proxy->updateTransactionMailBody($body, $editor, $xactions); } return; } /* -( Edit View )---------------------------------------------------------- */ /** * @task edit */ public function shouldAppearInEditView() { if ($this->proxy) { return $this->proxy->shouldAppearInEditView(); } return false; } /** * @task edit */ public function readValueFromRequest(AphrontRequest $request) { if ($this->proxy) { return $this->proxy->readValueFromRequest($request); } throw new PhabricatorCustomFieldImplementationIncompleteException($this); } /** * @task edit */ public function getRequiredHandlePHIDsForEdit() { if ($this->proxy) { return $this->proxy->getRequiredHandlePHIDsForEdit(); } return array(); } /** * @task edit */ public function getInstructionsForEdit() { if ($this->proxy) { return $this->proxy->getInstructionsForEdit(); } return null; } /** * @task edit */ public function renderEditControl(array $handles) { if ($this->proxy) { return $this->proxy->renderEditControl($handles); } throw new PhabricatorCustomFieldImplementationIncompleteException($this); } /* -( Property View )------------------------------------------------------ */ /** * @task view */ public function shouldAppearInPropertyView() { if ($this->proxy) { return $this->proxy->shouldAppearInPropertyView(); } return false; } /** * @task view */ public function renderPropertyViewLabel() { if ($this->proxy) { return $this->proxy->renderPropertyViewLabel(); } return $this->getFieldName(); } /** * @task view */ public function renderPropertyViewValue(array $handles) { if ($this->proxy) { return $this->proxy->renderPropertyViewValue($handles); } throw new PhabricatorCustomFieldImplementationIncompleteException($this); } /** * @task view */ public function getStyleForPropertyView() { if ($this->proxy) { return $this->proxy->getStyleForPropertyView(); } return 'property'; } /** * @task view */ public function getIconForPropertyView() { if ($this->proxy) { return $this->proxy->getIconForPropertyView(); } return null; } /** * @task view */ public function getRequiredHandlePHIDsForPropertyView() { if ($this->proxy) { return $this->proxy->getRequiredHandlePHIDsForPropertyView(); } return array(); } /* -( List View )---------------------------------------------------------- */ /** * @task list */ public function shouldAppearInListView() { if ($this->proxy) { return $this->proxy->shouldAppearInListView(); } return false; } /** * @task list */ public function renderOnListItem(PHUIObjectItemView $view) { if ($this->proxy) { return $this->proxy->renderOnListItem($view); } throw new PhabricatorCustomFieldImplementationIncompleteException($this); } /* -( Global Search )------------------------------------------------------ */ /** * @task globalsearch */ public function shouldAppearInGlobalSearch() { if ($this->proxy) { return $this->proxy->shouldAppearInGlobalSearch(); } return false; } /** * @task globalsearch */ public function updateAbstractDocument( PhabricatorSearchAbstractDocument $document) { if ($this->proxy) { return $this->proxy->updateAbstractDocument($document); } return $document; } /* -( Conduit )------------------------------------------------------------ */ /** * @task conduit */ public function shouldAppearInConduitDictionary() { if ($this->proxy) { return $this->proxy->shouldAppearInConduitDictionary(); } return false; } /** * @task conduit */ public function getConduitDictionaryValue() { if ($this->proxy) { return $this->proxy->getConduitDictionaryValue(); } throw new PhabricatorCustomFieldImplementationIncompleteException($this); } /* -( Herald )------------------------------------------------------------- */ /** * Return `true` to make this field available in Herald. * * @return bool True to expose the field in Herald. * @task herald */ public function shouldAppearInHerald() { if ($this->proxy) { return $this->proxy->shouldAppearInHerald(); } return false; } /** * Get the name of the field in Herald. By default, this uses the * normal field name. * * @return string Herald field name. * @task herald */ public function getHeraldFieldName() { if ($this->proxy) { return $this->proxy->getHeraldFieldName(); } return $this->getFieldName(); } /** * Get the field value for evaluation by Herald. * * @return wild Field value. * @task herald */ public function getHeraldFieldValue() { if ($this->proxy) { return $this->proxy->getHeraldFieldValue(); } throw new PhabricatorCustomFieldImplementationIncompleteException($this); } /** * Get the available conditions for this field in Herald. * * @return list List of Herald condition constants. * @task herald */ public function getHeraldFieldConditions() { if ($this->proxy) { return $this->proxy->getHeraldFieldConditions(); } throw new PhabricatorCustomFieldImplementationIncompleteException($this); } /** * Get the Herald value type for the given condition. * * @param const Herald condition constant. * @return const|null Herald value type, or null to use the default. * @task herald */ public function getHeraldFieldValueType($condition) { if ($this->proxy) { return $this->proxy->getHeraldFieldValueType($condition); } return null; } } diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomField.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomField.php index ccdb202f11..a75239bde5 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomField.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomField.php @@ -1,425 +1,424 @@ setAncestorClass(__CLASS__) ->loadObjects(); $types = mpull($types, null, 'getFieldType'); $fields = array(); foreach ($config as $key => $value) { $type = idx($value, 'type', 'text'); if (empty($types[$type])) { // TODO: We should have better typechecking somewhere, and then make // this more serious. continue; } $namespace = $template->getStandardCustomFieldNamespace(); $full_key = "std:{$namespace}:{$key}"; $template = clone $template; $standard = id(clone $types[$type]) ->setRawStandardFieldKey($key) ->setFieldKey($full_key) ->setFieldConfig($value) ->setApplicationField($template); $field = $template->setProxy($standard); $fields[] = $field; } return $fields; } public function setApplicationField( PhabricatorStandardCustomFieldInterface $application_field) { $this->applicationField = $application_field; return $this; } public function getApplicationField() { return $this->applicationField; } public function setFieldName($name) { $this->fieldName = $name; return $this; } public function getFieldValue() { return $this->fieldValue; } public function setFieldValue($value) { $this->fieldValue = $value; return $this; } public function setCaption($caption) { $this->caption = $caption; return $this; } public function getCaption() { return $this->caption; } public function setFieldDescription($description) { $this->fieldDescription = $description; return $this; } public function setFieldConfig(array $config) { foreach ($config as $key => $value) { switch ($key) { case 'name': $this->setFieldName($value); break; case 'description': $this->setFieldDescription($value); break; case 'strings': $this->setStrings($value); break; case 'caption': $this->setCaption($value); break; case 'required': if ($value) { $this->setRequired($value); $this->setFieldError(true); } break; case 'default': $this->setFieldValue($value); break; case 'type': // We set this earlier on. break; } } $this->fieldConfig = $config; return $this; } public function getFieldConfigValue($key, $default = null) { return idx($this->fieldConfig, $key, $default); } public function setFieldError($field_error) { $this->fieldError = $field_error; return $this; } public function getFieldError() { return $this->fieldError; } public function setRequired($required) { $this->required = $required; return $this; } public function getRequired() { return $this->required; } public function setRawStandardFieldKey($raw_key) { $this->rawKey = $raw_key; return $this; } public function getRawStandardFieldKey() { return $this->rawKey; } /* -( PhabricatorCustomField )--------------------------------------------- */ public function setFieldKey($field_key) { $this->fieldKey = $field_key; return $this; } public function getFieldKey() { return $this->fieldKey; } public function getFieldName() { return coalesce($this->fieldName, parent::getFieldName()); } public function getFieldDescription() { return coalesce($this->fieldDescription, parent::getFieldDescription()); } public function setStrings(array $strings) { $this->strings = $strings; return; } public function getString($key, $default = null) { return idx($this->strings, $key, $default); } public function shouldUseStorage() { return true; } public function getValueForStorage() { return $this->getFieldValue(); } public function setValueFromStorage($value) { return $this->setFieldValue($value); } public function shouldAppearInApplicationTransactions() { return true; } public function shouldAppearInEditView() { return $this->getFieldConfigValue('edit', true); } public function readValueFromRequest(AphrontRequest $request) { $value = $request->getStr($this->getFieldKey()); if (!strlen($value)) { $value = null; } $this->setFieldValue($value); } public function getInstructionsForEdit() { return $this->getFieldConfigValue('instructions'); } public function getPlaceholder() { return $this->getFieldConfigValue('placeholder', null); } public function renderEditControl(array $handles) { return id(new AphrontFormTextControl()) ->setName($this->getFieldKey()) ->setCaption($this->getCaption()) ->setValue($this->getFieldValue()) ->setError($this->getFieldError()) ->setLabel($this->getFieldName()) ->setPlaceholder($this->getPlaceholder()); } public function newStorageObject() { return $this->getApplicationField()->newStorageObject(); } public function shouldAppearInPropertyView() { return $this->getFieldConfigValue('view', true); } public function renderPropertyViewValue(array $handles) { if (!strlen($this->getFieldValue())) { return null; } return $this->getFieldValue(); } public function shouldAppearInApplicationSearch() { return $this->getFieldConfigValue('search', false); } protected function newStringIndexStorage() { return $this->getApplicationField()->newStringIndexStorage(); } protected function newNumericIndexStorage() { return $this->getApplicationField()->newNumericIndexStorage(); } public function buildFieldIndexes() { return array(); } public function buildOrderIndex() { return null; } public function readApplicationSearchValueFromRequest( PhabricatorApplicationSearchEngine $engine, AphrontRequest $request) { return; } public function applyApplicationSearchConstraintToQuery( PhabricatorApplicationSearchEngine $engine, PhabricatorCursorPagedPolicyAwareQuery $query, $value) { return; } public function appendToApplicationSearchForm( PhabricatorApplicationSearchEngine $engine, AphrontFormView $form, - $value, - array $handles) { + $value) { return; } public function validateApplicationTransactions( PhabricatorApplicationTransactionEditor $editor, $type, array $xactions) { $this->setFieldError(null); $errors = parent::validateApplicationTransactions( $editor, $type, $xactions); if ($this->getRequired()) { $value = $this->getOldValueForApplicationTransactions(); $transaction = null; foreach ($xactions as $xaction) { $value = $xaction->getNewValue(); if (!$this->isValueEmpty($value)) { $transaction = $xaction; break; } } if ($this->isValueEmpty($value)) { $error = new PhabricatorApplicationTransactionValidationError( $type, pht('Required'), pht('%s is required.', $this->getFieldName()), $transaction); $error->setIsMissingFieldError(true); $errors[] = $error; $this->setFieldError(pht('Required')); } } return $errors; } protected function isValueEmpty($value) { if (is_array($value)) { return empty($value); } return !strlen($value); } public function getApplicationTransactionTitle( PhabricatorApplicationTransaction $xaction) { $author_phid = $xaction->getAuthorPHID(); $old = $xaction->getOldValue(); $new = $xaction->getNewValue(); if (!$old) { return pht( '%s set %s to %s.', $xaction->renderHandleLink($author_phid), $this->getFieldName(), $new); } else if (!$new) { return pht( '%s removed %s.', $xaction->renderHandleLink($author_phid), $this->getFieldName()); } else { return pht( '%s changed %s from %s to %s.', $xaction->renderHandleLink($author_phid), $this->getFieldName(), $old, $new); } } public function getApplicationTransactionTitleForFeed( PhabricatorApplicationTransaction $xaction) { $author_phid = $xaction->getAuthorPHID(); $object_phid = $xaction->getObjectPHID(); $old = $xaction->getOldValue(); $new = $xaction->getNewValue(); if (!$old) { return pht( '%s set %s to %s on %s.', $xaction->renderHandleLink($author_phid), $this->getFieldName(), $new, $xaction->renderHandleLink($object_phid)); } else if (!$new) { return pht( '%s removed %s on %s.', $xaction->renderHandleLink($author_phid), $this->getFieldName(), $xaction->renderHandleLink($object_phid)); } else { return pht( '%s changed %s from %s to %s on %s.', $xaction->renderHandleLink($author_phid), $this->getFieldName(), $old, $new, $xaction->renderHandleLink($object_phid)); } } public function getHeraldFieldValue() { return $this->getFieldValue(); } public function getFieldControlID($key = null) { $key = coalesce($key, $this->getRawStandardFieldKey()); return 'std:control:'.$key; } public function shouldAppearInGlobalSearch() { return $this->getFieldConfigValue('fulltext', false); } public function updateAbstractDocument( PhabricatorSearchAbstractDocument $document) { $field_key = $this->getFieldConfigValue('fulltext'); // If the caller or configuration didn't specify a valid field key, // generate one automatically from the field index. if (!is_string($field_key) || (strlen($field_key) != 4)) { $field_key = '!'.substr($this->getFieldIndex(), 0, 3); } $field_value = $this->getFieldValue(); if (strlen($field_value)) { $document->addField($field_key, $field_value); } } } diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldBool.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldBool.php index 8ac1ce715e..29192e5c17 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldBool.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldBool.php @@ -1,133 +1,132 @@ getFieldValue(); if (strlen($value)) { $indexes[] = $this->newNumericIndex((int)$value); } return $indexes; } public function buildOrderIndex() { return $this->newNumericIndex(0); } public function readValueFromRequest(AphrontRequest $request) { $this->setFieldValue((bool)$request->getBool($this->getFieldKey())); } public function getValueForStorage() { $value = $this->getFieldValue(); if ($value !== null) { return (int)$value; } else { return null; } } public function setValueFromStorage($value) { if (strlen($value)) { $value = (bool)$value; } else { $value = null; } return $this->setFieldValue($value); } public function readApplicationSearchValueFromRequest( PhabricatorApplicationSearchEngine $engine, AphrontRequest $request) { return $request->getStr($this->getFieldKey()); } public function applyApplicationSearchConstraintToQuery( PhabricatorApplicationSearchEngine $engine, PhabricatorCursorPagedPolicyAwareQuery $query, $value) { if ($value == 'require') { $query->withApplicationSearchContainsConstraint( $this->newNumericIndex(null), 1); } } public function appendToApplicationSearchForm( PhabricatorApplicationSearchEngine $engine, AphrontFormView $form, - $value, - array $handles) { + $value) { $form->appendChild( id(new AphrontFormSelectControl()) ->setLabel($this->getFieldName()) ->setName($this->getFieldKey()) ->setValue($value) ->setOptions( array( '' => $this->getString('search.default', pht('(Any)')), 'require' => $this->getString('search.require', pht('Require')), ))); } public function renderEditControl(array $handles) { return id(new AphrontFormCheckboxControl()) ->setLabel($this->getFieldName()) ->setCaption($this->getCaption()) ->addCheckbox( $this->getFieldKey(), 1, $this->getString('edit.checkbox'), (bool)$this->getFieldValue()); } public function renderPropertyViewValue(array $handles) { $value = $this->getFieldValue(); if ($value) { return $this->getString('view.yes', pht('Yes')); } else { return null; } } public function getApplicationTransactionTitle( PhabricatorApplicationTransaction $xaction) { $author_phid = $xaction->getAuthorPHID(); $old = $xaction->getOldValue(); $new = $xaction->getNewValue(); if ($new) { return pht( '%s checked %s.', $xaction->renderHandleLink($author_phid), $this->getFieldName()); } else { return pht( '%s unchecked %s.', $xaction->renderHandleLink($author_phid), $this->getFieldName()); } } public function shouldAppearInHerald() { return true; } public function getHeraldFieldConditions() { return array( HeraldAdapter::CONDITION_IS_TRUE, HeraldAdapter::CONDITION_IS_FALSE, ); } } diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldDate.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldDate.php index f8222d9559..c5b86bb097 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldDate.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldDate.php @@ -1,195 +1,194 @@ getFieldValue(); if (strlen($value)) { $indexes[] = $this->newNumericIndex((int)$value); } return $indexes; } public function buildOrderIndex() { return $this->newNumericIndex(0); } public function getValueForStorage() { $value = $this->getFieldValue(); if (strlen($value)) { return (int)$value; } else { return null; } } public function setValueFromStorage($value) { if (strlen($value)) { $value = (int)$value; } else { $value = null; } return $this->setFieldValue($value); } public function renderEditControl(array $handles) { return $this->newDateControl(); } public function readValueFromRequest(AphrontRequest $request) { $control = $this->newDateControl(); $control->setUser($request->getUser()); $value = $control->readValueFromRequest($request); $this->setFieldValue($value); } public function renderPropertyViewValue(array $handles) { $value = $this->getFieldValue(); if (!$value) { return null; } return phabricator_datetime($value, $this->getViewer()); } private function newDateControl() { $control = id(new AphrontFormDateControl()) ->setLabel($this->getFieldName()) ->setName($this->getFieldKey()) ->setUser($this->getViewer()) ->setCaption($this->getCaption()) ->setAllowNull(!$this->getRequired()); // If the value is already numeric, treat it as an epoch timestamp and set // it directly. Otherwise, it's likely a field default, which we let users // specify as a string. Parse the string into an epoch. $value = $this->getFieldValue(); if (!ctype_digit($value)) { $value = PhabricatorTime::parseLocalTime($value, $this->getViewer()); } // If we don't have anything valid, make sure we pass `null`, since the // control special-cases that. $control->setValue(nonempty($value, null)); return $control; } public function readApplicationSearchValueFromRequest( PhabricatorApplicationSearchEngine $engine, AphrontRequest $request) { $key = $this->getFieldKey(); return array( 'min' => $request->getStr($key.'.min'), 'max' => $request->getStr($key.'.max'), ); } public function applyApplicationSearchConstraintToQuery( PhabricatorApplicationSearchEngine $engine, PhabricatorCursorPagedPolicyAwareQuery $query, $value) { $viewer = $this->getViewer(); if (!is_array($value)) { $value = array(); } $min_str = idx($value, 'min', ''); if (strlen($min_str)) { $min = PhabricatorTime::parseLocalTime($min_str, $viewer); } else { $min = null; } $max_str = idx($value, 'max', ''); if (strlen($max_str)) { $max = PhabricatorTime::parseLocalTime($max_str, $viewer); } else { $max = null; } if (($min !== null) || ($max !== null)) { $query->withApplicationSearchRangeConstraint( $this->newNumericIndex(null), $min, $max); } } public function appendToApplicationSearchForm( PhabricatorApplicationSearchEngine $engine, AphrontFormView $form, - $value, - array $handles) { + $value) { if (!is_array($value)) { $value = array(); } $form ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('%s After', $this->getFieldName())) ->setName($this->getFieldKey().'.min') ->setValue(idx($value, 'min', ''))) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('%s Before', $this->getFieldName())) ->setName($this->getFieldKey().'.max') ->setValue(idx($value, 'max', ''))); } public function getApplicationTransactionTitle( PhabricatorApplicationTransaction $xaction) { $author_phid = $xaction->getAuthorPHID(); $old = $xaction->getOldValue(); $new = $xaction->getNewValue(); $viewer = $this->getViewer(); $old_date = null; if ($old) { $old_date = phabricator_datetime($old, $viewer); } $new_date = null; if ($new) { $new_date = phabricator_datetime($new, $viewer); } if (!$old) { return pht( '%s set %s to %s.', $xaction->renderHandleLink($author_phid), $this->getFieldName(), $new_date); } else if (!$new) { return pht( '%s removed %s.', $xaction->renderHandleLink($author_phid), $this->getFieldName()); } else { return pht( '%s changed %s from %s to %s.', $xaction->renderHandleLink($author_phid), $this->getFieldName(), $old_date, $new_date); } } } diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldInt.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldInt.php index 94a24d01b0..cbb89275f9 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldInt.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldInt.php @@ -1,117 +1,116 @@ getFieldValue(); if (strlen($value)) { $indexes[] = $this->newNumericIndex((int)$value); } return $indexes; } public function buildOrderIndex() { return $this->newNumericIndex(0); } public function getValueForStorage() { $value = $this->getFieldValue(); if (strlen($value)) { return $value; } else { return null; } } public function setValueFromStorage($value) { if (strlen($value)) { $value = (int)$value; } else { $value = null; } return $this->setFieldValue($value); } public function readApplicationSearchValueFromRequest( PhabricatorApplicationSearchEngine $engine, AphrontRequest $request) { return $request->getStr($this->getFieldKey()); } public function applyApplicationSearchConstraintToQuery( PhabricatorApplicationSearchEngine $engine, PhabricatorCursorPagedPolicyAwareQuery $query, $value) { if (strlen($value)) { $query->withApplicationSearchContainsConstraint( $this->newNumericIndex(null), $value); } } public function appendToApplicationSearchForm( PhabricatorApplicationSearchEngine $engine, AphrontFormView $form, - $value, - array $handles) { + $value) { $form->appendChild( id(new AphrontFormTextControl()) ->setLabel($this->getFieldName()) ->setName($this->getFieldKey()) ->setValue($value)); } public function validateApplicationTransactions( PhabricatorApplicationTransactionEditor $editor, $type, array $xactions) { $errors = parent::validateApplicationTransactions( $editor, $type, $xactions); foreach ($xactions as $xaction) { $value = $xaction->getNewValue(); if (strlen($value)) { if (!preg_match('/^-?\d+/', $value)) { $errors[] = new PhabricatorApplicationTransactionValidationError( $type, pht('Invalid'), pht('%s must be an integer.', $this->getFieldName()), $xaction); $this->setFieldError(pht('Invalid')); } } } return $errors; } public function getApplicationTransactionHasEffect( PhabricatorApplicationTransaction $xaction) { $old = $xaction->getOldValue(); $new = $xaction->getNewValue(); if (!strlen($old) && strlen($new)) { return true; } else if (strlen($old) && !strlen($new)) { return true; } else { return ((int)$old !== (int)$new); } } } diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldLink.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldLink.php index 8244f77a33..cd16c930d7 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldLink.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldLink.php @@ -1,84 +1,83 @@ getFieldValue(); if (strlen($value)) { $indexes[] = $this->newStringIndex($value); } return $indexes; } public function renderPropertyViewValue(array $handles) { $value = $this->getFieldValue(); if (!strlen($value)) { return null; } if (!PhabricatorEnv::isValidRemoteURIForLink($value)) { return $value; } return phutil_tag( 'a', array('href' => $value, 'target' => '_blank'), $value); } public function readApplicationSearchValueFromRequest( PhabricatorApplicationSearchEngine $engine, AphrontRequest $request) { return $request->getStr($this->getFieldKey()); } public function applyApplicationSearchConstraintToQuery( PhabricatorApplicationSearchEngine $engine, PhabricatorCursorPagedPolicyAwareQuery $query, $value) { if (strlen($value)) { $query->withApplicationSearchContainsConstraint( $this->newStringIndex(null), $value); } } public function appendToApplicationSearchForm( PhabricatorApplicationSearchEngine $engine, AphrontFormView $form, - $value, - array $handles) { + $value) { $form->appendChild( id(new AphrontFormTextControl()) ->setLabel($this->getFieldName()) ->setName($this->getFieldKey()) ->setValue($value)); } public function shouldAppearInHerald() { return true; } public function getHeraldFieldConditions() { return array( HeraldAdapter::CONDITION_CONTAINS, HeraldAdapter::CONDITION_NOT_CONTAINS, HeraldAdapter::CONDITION_IS, HeraldAdapter::CONDITION_IS_NOT, HeraldAdapter::CONDITION_REGEXP, ); } } diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldPHIDs.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldPHIDs.php index 68b5f44a77..82087bbaee 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldPHIDs.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldPHIDs.php @@ -1,175 +1,168 @@ getFieldValue(); if (is_array($value)) { foreach ($value as $phid) { $indexes[] = $this->newStringIndex($phid); } } return $indexes; } public function readValueFromRequest(AphrontRequest $request) { $value = $request->getArr($this->getFieldKey()); $this->setFieldValue($value); } public function getValueForStorage() { $value = $this->getFieldValue(); if (!$value) { return null; } return json_encode(array_values($value)); } public function setValueFromStorage($value) { $result = array(); if ($value) { $value = json_decode($value, true); if (is_array($value)) { $result = array_values($value); } } $this->setFieldValue($value); } public function readApplicationSearchValueFromRequest( PhabricatorApplicationSearchEngine $engine, AphrontRequest $request) { return $request->getArr($this->getFieldKey()); } public function applyApplicationSearchConstraintToQuery( PhabricatorApplicationSearchEngine $engine, PhabricatorCursorPagedPolicyAwareQuery $query, $value) { if ($value) { $query->withApplicationSearchContainsConstraint( $this->newStringIndex(null), $value); } } - public function getRequiredHandlePHIDsForApplicationSearch($value) { - if ($value) { - return $value; - } - return array(); - } - public function getRequiredHandlePHIDsForPropertyView() { $value = $this->getFieldValue(); if ($value) { return $value; } return array(); } public function renderPropertyViewValue(array $handles) { $value = $this->getFieldValue(); if (!$value) { return null; } $handles = mpull($handles, 'renderLink'); $handles = phutil_implode_html(', ', $handles); return $handles; } public function getRequiredHandlePHIDsForEdit() { $value = $this->getFieldValue(); if ($value) { return $value; } else { return array(); } } public function getApplicationTransactionRequiredHandlePHIDs( PhabricatorApplicationTransaction $xaction) { $old = json_decode($xaction->getOldValue()); if (!is_array($old)) { $old = array(); } $new = json_decode($xaction->getNewValue()); if (!is_array($new)) { $new = array(); } $add = array_diff($new, $old); $rem = array_diff($old, $new); return array_merge($add, $rem); } public function getApplicationTransactionTitle( PhabricatorApplicationTransaction $xaction) { $author_phid = $xaction->getAuthorPHID(); $old = json_decode($xaction->getOldValue()); if (!is_array($old)) { $old = array(); } $new = phutil_json_decode($xaction->getNewValue()); if (!is_array($new)) { $new = array(); } $add = array_diff($new, $old); $rem = array_diff($old, $new); if ($add && !$rem) { return pht( '%s updated %s, added %d: %s.', $xaction->renderHandleLink($author_phid), $this->getFieldName(), new PhutilNumber(count($add)), $xaction->renderHandleList($add)); } else if ($rem && !$add) { return pht( '%s updated %s, removed %d: %s.', $xaction->renderHandleLink($author_phid), $this->getFieldName(), new PhutilNumber(count($rem)), $xaction->renderHandleList($rem)); } else { return pht( '%s updated %s, added %d: %s; removed %d: %s.', $xaction->renderHandleLink($author_phid), $this->getFieldName(), new PhutilNumber(count($add)), $xaction->renderHandleList($add), new PhutilNumber(count($rem)), $xaction->renderHandleList($rem)); } } public function shouldAppearInHerald() { return true; } public function getHeraldFieldConditions() { return array( HeraldAdapter::CONDITION_INCLUDE_ALL, HeraldAdapter::CONDITION_INCLUDE_ANY, HeraldAdapter::CONDITION_INCLUDE_NONE, HeraldAdapter::CONDITION_EXISTS, HeraldAdapter::CONDITION_NOT_EXISTS, ); } } diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldSelect.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldSelect.php index ec1856aa79..42e579801e 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldSelect.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldSelect.php @@ -1,114 +1,113 @@ getFieldValue(); if (strlen($value)) { $indexes[] = $this->newStringIndex($value); } return $indexes; } public function readApplicationSearchValueFromRequest( PhabricatorApplicationSearchEngine $engine, AphrontRequest $request) { return $request->getArr($this->getFieldKey()); } public function applyApplicationSearchConstraintToQuery( PhabricatorApplicationSearchEngine $engine, PhabricatorCursorPagedPolicyAwareQuery $query, $value) { if ($value) { $query->withApplicationSearchContainsConstraint( $this->newStringIndex(null), $value); } } public function appendToApplicationSearchForm( PhabricatorApplicationSearchEngine $engine, AphrontFormView $form, - $value, - array $handles) { + $value) { if (!is_array($value)) { $value = array(); } $value = array_fuse($value); $control = id(new AphrontFormCheckboxControl()) ->setLabel($this->getFieldName()); foreach ($this->getOptions() as $name => $option) { $control->addCheckbox( $this->getFieldKey().'[]', $name, $option, isset($value[$name])); } $form->appendChild($control); } private function getOptions() { return $this->getFieldConfigValue('options', array()); } public function renderEditControl(array $handles) { return id(new AphrontFormSelectControl()) ->setLabel($this->getFieldName()) ->setCaption($this->getCaption()) ->setName($this->getFieldKey()) ->setValue($this->getFieldValue()) ->setOptions($this->getOptions()); } public function renderPropertyViewValue(array $handles) { if (!strlen($this->getFieldValue())) { return null; } return idx($this->getOptions(), $this->getFieldValue()); } public function getApplicationTransactionTitle( PhabricatorApplicationTransaction $xaction) { $author_phid = $xaction->getAuthorPHID(); $old = $xaction->getOldValue(); $new = $xaction->getNewValue(); $old = idx($this->getOptions(), $old, $old); $new = idx($this->getOptions(), $new, $new); if (!$old) { return pht( '%s set %s to %s.', $xaction->renderHandleLink($author_phid), $this->getFieldName(), $new); } else if (!$new) { return pht( '%s removed %s.', $xaction->renderHandleLink($author_phid), $this->getFieldName()); } else { return pht( '%s changed %s from %s to %s.', $xaction->renderHandleLink($author_phid), $this->getFieldName(), $old, $new); } } } diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldText.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldText.php index 92967c8de9..ca519b0c79 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldText.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldText.php @@ -1,67 +1,66 @@ getFieldValue(); if (strlen($value)) { $indexes[] = $this->newStringIndex($value); } return $indexes; } public function readApplicationSearchValueFromRequest( PhabricatorApplicationSearchEngine $engine, AphrontRequest $request) { return $request->getStr($this->getFieldKey()); } public function applyApplicationSearchConstraintToQuery( PhabricatorApplicationSearchEngine $engine, PhabricatorCursorPagedPolicyAwareQuery $query, $value) { if (strlen($value)) { $query->withApplicationSearchContainsConstraint( $this->newStringIndex(null), $value); } } public function appendToApplicationSearchForm( PhabricatorApplicationSearchEngine $engine, AphrontFormView $form, - $value, - array $handles) { + $value) { $form->appendChild( id(new AphrontFormTextControl()) ->setLabel($this->getFieldName()) ->setName($this->getFieldKey()) ->setValue($value)); } public function shouldAppearInHerald() { return true; } public function getHeraldFieldConditions() { return array( HeraldAdapter::CONDITION_CONTAINS, HeraldAdapter::CONDITION_NOT_CONTAINS, HeraldAdapter::CONDITION_IS, HeraldAdapter::CONDITION_IS_NOT, HeraldAdapter::CONDITION_REGEXP, ); } } diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldUsers.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldUsers.php index 7ea66a8a68..87dc7f830c 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldUsers.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldUsers.php @@ -1,48 +1,47 @@ getFieldValue(); $control = id(new AphrontFormTokenizerControl()) ->setUser($this->getViewer()) ->setLabel($this->getFieldName()) ->setName($this->getFieldKey()) ->setDatasource(new PhabricatorPeopleDatasource()) ->setCaption($this->getCaption()) ->setValue(nonempty($value, array())); $limit = $this->getFieldConfigValue('limit'); if ($limit) { $control->setLimit($limit); } return $control; } public function appendToApplicationSearchForm( PhabricatorApplicationSearchEngine $engine, AphrontFormView $form, - $value, - array $handles) { + $value) { $control = id(new AphrontFormTokenizerControl()) ->setLabel($this->getFieldName()) ->setName($this->getFieldKey()) ->setDatasource(new PhabricatorPeopleDatasource()) ->setValue(nonempty($value, array())); $form->appendControl($control); } public function getHeraldFieldValueType($condition) { return HeraldAdapter::VALUE_USER; } }