diff --git a/search_api.links.action.yml b/search_api.links.action.yml index 7d85b506..5b99f87f 100644 --- a/search_api.links.action.yml +++ b/search_api.links.action.yml @@ -13,8 +13,3 @@ search_api.execute_tasks: title: 'Execute pending tasks' appears_on: - search_api.overview -entity.search_api_index.add_fields: - route_name: entity.search_api_index.add_fields - title: 'Add fields' - appears_on: - - entity.search_api_index.fields diff --git a/search_api.routing.yml b/search_api.routing.yml index 34c34480..5e349b80 100644 --- a/search_api.routing.yml +++ b/search_api.routing.yml @@ -124,7 +124,19 @@ entity.search_api_index.fields: _entity_access: 'search_api_index.fields' entity.search_api_index.add_fields: - path: '/admin/config/search/search-api/index/{search_api_index}/fields/add' + path: '/admin/config/search/search-api/index/{search_api_index}/fields/add/nojs' + options: + parameters: + search_api_index: + tempstore: TRUE + type: 'entity:search_api_index' + defaults: + _entity_form: 'search_api_index.add_fields' + requirements: + _entity_access: 'search_api_index.fields' + +entity.search_api_index.add_fields_ajax: + path: '/admin/config/search/search-api/index/{search_api_index}/fields/add/ajax' options: parameters: search_api_index: @@ -136,7 +148,7 @@ entity.search_api_index.add_fields: _entity_access: 'search_api_index.fields' entity.search_api_index.field_config: - path: '/admin/config/search/search-api/index/{search_api_index}/fields/{field_id}/edit' + path: '/admin/config/search/search-api/index/{search_api_index}/fields/edit/{field_id}' options: parameters: search_api_index: @@ -149,7 +161,7 @@ entity.search_api_index.field_config: _entity_access: 'search_api_index.fields' entity.search_api_index.remove_field: - path: '/admin/config/search/search-api/index/{search_api_index}/fields/{field_id}/remove' + path: '/admin/config/search/search-api/index/{search_api_index}/fields/remove/{field_id}/nojs' options: parameters: search_api_index: @@ -161,6 +173,19 @@ entity.search_api_index.remove_field: _entity_access: 'search_api_index.fields' _csrf_token: 'TRUE' +entity.search_api_index.remove_field_ajax: + path: '/admin/config/search/search-api/index/{search_api_index}/fields/remove/{field_id}/ajax' + options: + parameters: + search_api_index: + tempstore: TRUE + type: 'entity:search_api_index' + defaults: + _controller: 'Drupal\search_api\Controller\IndexController::removeFieldAjax' + requirements: + _entity_access: 'search_api_index.fields' + _csrf_token: 'TRUE' + entity.search_api_index.break_lock_form: path: '/admin/config/search/search-api/index/{search_api_index}/fields/break-lock' defaults: diff --git a/search_api.theme.inc b/search_api.theme.inc index ebb0bd7e..0212e867 100644 --- a/search_api.theme.inc +++ b/search_api.theme.inc @@ -41,6 +41,7 @@ function theme_search_api_admin_fields_table($variables) { else { $rows[] = array( 'data' => $row, + 'data-field-row-id' => $name, 'title' => strip_tags($form['fields'][$name]['description']['#value']), ); } @@ -160,6 +161,9 @@ function theme_search_api_form_item_list(array $variables) { $build = array( '#theme' => 'item_list', ); + if (!empty($element['#title'])) { + $build['#title'] = $element['#title']; + } foreach (Element::children($element) as $key) { $build['#items'][$key] = $element[$key]; } diff --git a/src/Controller/IndexController.php b/src/Controller/IndexController.php index 07bdebdc..875749e1 100644 --- a/src/Controller/IndexController.php +++ b/src/Controller/IndexController.php @@ -3,9 +3,14 @@ namespace Drupal\search_api\Controller; use Drupal\Component\Render\FormattableMarkup; +use Drupal\Core\Ajax\AjaxResponse; +use Drupal\Core\Ajax\RemoveCommand; use Drupal\Core\Controller\ControllerBase; +use Drupal\Core\EventSubscriber\AjaxResponseSubscriber; use Drupal\search_api\IndexInterface; use Drupal\search_api\SearchApiException; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** @@ -14,6 +19,57 @@ class IndexController extends ControllerBase { /** + * The request stack. + * + * @var \Symfony\Component\HttpFoundation\RequestStack|null + */ + protected $requestStack; + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + /** @var static $controller */ + $controller = parent::create($container); + + $controller->setRequestStack($container->get('request_stack')); + + return $controller; + } + + /** + * Retrieves the request stack. + * + * @return \Symfony\Component\HttpFoundation\RequestStack + * The request stack. + */ + public function getRequestStack() { + return $this->requestStack ?: \Drupal::service('request_stack'); + } + + /** + * Retrieves the current request. + * + * @return \Symfony\Component\HttpFoundation\Request|null + */ + public function getRequest() { + return $this->getRequestStack()->getCurrentRequest(); + } + + /** + * Sets the request stack. + * + * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack + * The new request stack. + * + * @return $this + */ + public function setRequestStack(RequestStack $request_stack) { + $this->requestStack = $request_stack; + return $this; + } + + /** * Displays information about a search index. * * @param \Drupal\search_api\IndexInterface $search_api_index @@ -110,9 +166,45 @@ public function removeField(IndexInterface $search_api_index, $field_id) { throw new NotFoundHttpException(); } - // Redirect to the index's "View" page. + // Redirect to the index's "Fields" page. $url = $search_api_index->toUrl('fields'); return $this->redirect($url->getRouteName(), $url->getRouteParameters()); } + /** + * Removes a field from a search index using AJAX. + * + * @param \Drupal\search_api\IndexInterface $search_api_index + * The search index. + * @param string $field_id + * The ID of the field to remove. + * + * @return \Symfony\Component\HttpFoundation\Response + * The response to send to the browser. + * + * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException + * Thrown when the field was not found. + */ + public function removeFieldAjax(IndexInterface $search_api_index, $field_id) { + $fields = $search_api_index->getFields(); + if (isset($fields[$field_id])) { + try { + $search_api_index->removeField($field_id); + $search_api_index->save(); + + $response = new AjaxResponse(); + $response->addCommand(new RemoveCommand("tr[data-field-row-id='$field_id']")); + return $response; + } + catch (SearchApiException $e) { + $args['%field'] = $fields[$field_id]->getLabel(); + drupal_set_message($this->t('The field %field is locked and cannot be removed.', $args), 'error'); + + $url = $search_api_index->toUrl('fields'); + return $this->redirect($url->getRouteName(), $url->getRouteParameters()); + } + } + throw new NotFoundHttpException(); + } + } diff --git a/src/Entity/Index.php b/src/Entity/Index.php index bbe53e78..69fe5192 100644 --- a/src/Entity/Index.php +++ b/src/Entity/Index.php @@ -78,7 +78,8 @@ * "add-form" = "/admin/config/search/search-api/add-index", * "edit-form" = "/admin/config/search/search-api/index/{search_api_index}/edit", * "fields" = "/admin/config/search/search-api/index/{search_api_index}/fields", - * "add-fields" = "/admin/config/search/search-api/index/{search_api_index}/fields/add", + * "add-fields" = "/admin/config/search/search-api/index/{search_api_index}/fields/add/nojs", + * "add-fields-ajax" = "/admin/config/search/search-api/index/{search_api_index}/fields/add/ajax", * "break-lock-form" = "/admin/config/search/search-api/index/{search_api_index}/fields/break-lock", * "processors" = "/admin/config/search/search-api/index/{search_api_index}/processors", * "delete-form" = "/admin/config/search/search-api/index/{search_api_index}/delete", diff --git a/src/Form/FieldConfigurationForm.php b/src/Form/FieldConfigurationForm.php index c81912f5..367bbc9b 100644 --- a/src/Form/FieldConfigurationForm.php +++ b/src/Form/FieldConfigurationForm.php @@ -2,11 +2,15 @@ namespace Drupal\search_api\Form; +use Drupal\Component\Render\FormattableMarkup; +use Drupal\Component\Utility\Html; use Drupal\Core\Datetime\DateFormatterInterface; use Drupal\Core\Entity\EntityForm; use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\EventSubscriber\MainContentViewSubscriber; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Render\RendererInterface; +use Drupal\Core\Url; use Drupal\search_api\Processor\ConfigurablePropertyInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\RequestStack; @@ -33,6 +37,13 @@ class FieldConfigurationForm extends EntityForm { protected $field; /** + * The "id" attribute of the generated form. + * + * @var string + */ + protected $formIdAttribute; + + /** * {@inheritdoc} */ public function getFormId() { @@ -76,9 +87,6 @@ public static function create(ContainerInterface $container) { public function buildForm(array $form, FormStateInterface $form_state) { $field = $this->getField(); - $args['%field'] = $field->getLabel(); - $form['#title'] = $this->t('Edit field %field', $args); - if (!$field) { $args['@id'] = $this->getRequest()->attributes->get('field_id'); $form['message'] = array( @@ -87,6 +95,21 @@ public function buildForm(array $form, FormStateInterface $form_state) { return $form; } + $args['%field'] = $field->getLabel(); + $form['#title'] = $this->t('Edit field %field', $args); + + if ($this->getRequest()->query->get('modal_redirect')) { + $form['title']['#markup'] = new FormattableMarkup('

@title

', ['@title' => $form['#title']]); + Html::setIsAjax(TRUE); + } + + $this->formIdAttribute = Html::getUniqueId($this->getFormId()); + $form['#id'] = $this->formIdAttribute; + + $form['messages'] = [ + '#type' => 'status_messages', + ]; + $property = $field->getDataDefinition(); if (!($property instanceof ConfigurablePropertyInterface)) { $args['%field'] = $field->getLabel(); @@ -123,11 +146,16 @@ protected function actions(array $form, FormStateInterface $form_state) { $actions = parent::actions($form, $form_state); unset($actions['delete']); - $actions['cancel'] = array( - '#type' => 'link', - '#title' => $this->t('Cancel'), - '#url' => $this->entity->toUrl('fields'), - ); + if ($this->getRequest()->query->get('modal_redirect')) { + $actions['submit']['#ajax']['wrapper'] = $this->formIdAttribute; + } + else { + $actions['cancel'] = array( + '#type' => 'link', + '#title' => $this->t('Cancel'), + '#url' => $this->entity->toUrl('fields'), + ); + } return $actions; } @@ -152,7 +180,16 @@ public function submitForm(array &$form, FormStateInterface $form_state) { $property->submitConfigurationForm($field, $form, $form_state); drupal_set_message($this->t('The field configuration was successfully saved.')); - $form_state->setRedirectUrl($this->entity->toUrl('fields')); + if ($this->getRequest()->query->get('modal_redirect')) { + $url = $this->entity->toUrl('add-fields-ajax') + ->setOption('query', [ + MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax', + ]); + $form_state->setRedirectUrl($url); + } + else { + $form_state->setRedirectUrl($this->entity->toUrl('fields')); + } } /** diff --git a/src/Form/IndexAddFieldsForm.php b/src/Form/IndexAddFieldsForm.php index 2de567f2..b5faaa61 100644 --- a/src/Form/IndexAddFieldsForm.php +++ b/src/Form/IndexAddFieldsForm.php @@ -8,6 +8,8 @@ use Drupal\Core\Entity\EntityForm; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Entity\TypedData\EntityDataDefinitionInterface; +use Drupal\Core\EventSubscriber\AjaxResponseSubscriber; +use Drupal\Core\EventSubscriber\MainContentViewSubscriber; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Render\RendererInterface; use Drupal\Core\TypedData\ComplexDataDefinitionInterface; @@ -59,6 +61,13 @@ class IndexAddFieldsForm extends EntityForm { protected $unmappedFields = array(); /** + * The "id" attribute of the generated form. + * + * @var string + */ + protected $formIdAttribute; + + /** * {@inheritdoc} */ public function getFormId() { @@ -138,15 +147,22 @@ public function buildForm(array $form, FormStateInterface $form_state) { $args['%index'] = $index->label(); $form['#title'] = $this->t('Add fields to index %index', $args); - $form['properties'] = array( - '#theme' => 'search_api_form_item_list', - ); + $this->formIdAttribute = Html::getUniqueId($this->getFormId()); + $form['#id'] = $this->formIdAttribute; + + $form['messages'] = [ + '#type' => 'status_messages', + ]; + $datasources = array( '' => NULL, ); $datasources += $this->entity->getDatasources(); foreach ($datasources as $datasource) { - $form['properties'][] = $this->getDatasourceListItem($datasource); + $item = $this->getDatasourceListItem($datasource); + if ($item) { + $form['datasources'][] = $item; + } } $form['actions'] = $this->actionsElement($form, $form_state); @@ -188,52 +204,24 @@ public function buildForm(array $form, FormStateInterface $form_state) { * attached properties. */ protected function getDatasourceListItem(DatasourceInterface $datasource = NULL) { - $item = array( - '#type' => 'container', - '#attributes' => array( - 'class' => array('container-inline'), - ), - ); - - $active = FALSE; $datasource_id = $datasource ? $datasource->getPluginId() : ''; - $active_datasource = $this->getParameter('datasource'); - if (isset($active_datasource)) { - $active = $active_datasource == $datasource_id; - } - - $url = $this->entity->toUrl('add-fields'); - if ($active) { - $expand_link = array( - '#type' => 'link', - '#title' => '(-) ', - '#url' => $url, - ); - } - else { - $url->setOption('query', array('datasource' => $datasource_id)); - $expand_link = array( - '#type' => 'link', - '#title' => '(+) ', - '#url' => $url, - ); - } - $item['expand_link'] = $expand_link; - - $label = $datasource ? Html::escape($datasource->label()) : $this->t('General'); - $item['label']['#markup'] = $label; - - if ($active) { - $properties = $this->entity->getPropertyDefinitions($datasource_id ?: NULL); - if ($properties) { + $properties = $this->entity->getPropertyDefinitions($datasource_id ?: NULL); + if ($properties) { + $active_property_path = ''; + $active_datasource = $this->getParameter('datasource'); + if ($active_datasource !== NULL && $active_datasource == $datasource_id) { $active_property_path = $this->getParameter('property_path', ''); - $base_url = clone $url; - $base_url->setOption('query', array('datasource' => $datasource_id)); - $item['properties'] = $this->getPropertiesList($properties, $active_property_path, $base_url); } + + $base_url = $this->entity->toUrl('add-fields'); + $base_url->setOption('query', array('datasource' => $datasource_id)); + + $item = $this->getPropertiesList($properties, $active_property_path, $base_url); + $item['#title'] = $datasource ? $datasource->label() : $this->t('General'); + return $item; } - return $item; + return NULL; } /** @@ -377,6 +365,9 @@ protected function getPropertiesList(array $properties, $active_property_path, U '#property' => $property, '#prefixed_label' => $label_prefix . $label, '#data_type' => $type_mapping[$type], + '#ajax' => array( + 'wrapper' => $this->formIdAttribute, + ), ); } @@ -433,7 +424,15 @@ public function addField(array $form, FormStateInterface $form_state) { 'search_api_index' => $this->entity->id(), 'field_id' => $field->getFieldIdentifier(), ); - $form_state->setRedirect('entity.search_api_index.field_config', $parameters); + $options = array(); + $route = $this->getRequest()->attributes->get('_route'); + if ($route === 'entity.search_api_index.add_fields_ajax') { + $options['query'] = [ + MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax', + 'modal_redirect' => 1, + ]; + } + $form_state->setRedirect('entity.search_api_index.field_config', $parameters, $options); } $args['%label'] = $field->getLabel(); diff --git a/src/Form/IndexFieldsForm.php b/src/Form/IndexFieldsForm.php index a3128072..ae924354 100644 --- a/src/Form/IndexFieldsForm.php +++ b/src/Form/IndexFieldsForm.php @@ -2,6 +2,7 @@ namespace Drupal\search_api\Form; +use Drupal\Component\Serialization\Json; use Drupal\Component\Utility\Html; use Drupal\Core\Datetime\DateFormatterInterface; use Drupal\Core\Entity\EntityForm; @@ -11,6 +12,7 @@ use Drupal\Core\Url; use Drupal\search_api\DataType\DataTypePluginManager; use Drupal\search_api\Processor\ConfigurablePropertyInterface; +use Drupal\search_api\SearchApiException; use Drupal\search_api\UnsavedConfigurationInterface; use Drupal\search_api\Utility\Utility; use Drupal\user\SharedTempStoreFactory; @@ -107,6 +109,8 @@ public function getDataTypePluginManager() { * {@inheritdoc} */ public function buildForm(array $form, FormStateInterface $form_state) { + $form['#attached']['library'][] = 'core/drupal.dialog.ajax'; + $index = $this->entity; // Do not allow the form to be cached. See @@ -119,6 +123,25 @@ public function buildForm(array $form, FormStateInterface $form_state) { $form['#title'] = $this->t('Manage fields for search index %label', array('%label' => $index->label())); $form['#tree'] = TRUE; + $form['add-field'] = [ + '#type' => 'link', + '#title' => $this->t('Add fields'), + '#url' => $this->entity->toUrl('add-fields'), + '#attributes' => [ + 'class' => [ + 'use-ajax', + 'button', + 'button-action', + 'button--primary', + 'button--small', + ], + 'data-dialog-type' => 'modal', + 'data-dialog-options' => Json::encode([ + 'width' => 700, + ]), + ], + ]; + $form['description']['#markup'] = $this->t('

The data type of a field determines how it can be used for searching and filtering. The boost is used to give additional weight to certain fields, for example titles or tags.

For information about the data types available for indexing, see the data types table at the bottom of the page.

', array('@url' => '#search-api-data-types-table')); if ($fields = $index->getFieldsByDatasource(NULL)) { @@ -284,12 +307,26 @@ protected function buildFieldsTable(array $fields) { // don't break the table structure. (theme_search_api_admin_fields_table() // does not add empty cells.) $build['fields'][$key]['edit']['#markup'] = ''; - if ($field->getDataDefinition() instanceof ConfigurablePropertyInterface) { - $build['fields'][$key]['edit'] = array( - '#type' => 'link', - '#title' => $this->t('Edit'), - '#url' => Url::fromRoute('entity.search_api_index.field_config', $route_parameters), - ); + try { + if ($field->getDataDefinition() instanceof ConfigurablePropertyInterface) { + $build['fields'][$key]['edit'] = array( + '#type' => 'link', + '#title' => $this->t('Edit'), + '#url' => Url::fromRoute('entity.search_api_index.field_config', $route_parameters), + '#attributes' => [ + 'class' => [ + 'use-ajax', + ], + 'data-dialog-type' => 'modal', + 'data-dialog-options' => Json::encode([ + 'width' => 700, + ]), + ], + ); + } + } + catch (SearchApiException $e) { + // Could not retrieve data definition: ignore. } $build['fields'][$key]['remove']['#markup'] = ''; if (!$field->isIndexedLocked()) { @@ -297,6 +334,9 @@ protected function buildFieldsTable(array $fields) { '#type' => 'link', '#title' => $this->t('Remove'), '#url' => Url::fromRoute('entity.search_api_index.remove_field', $route_parameters), + '#ajax' => [ + 'url' => Url::fromRoute('entity.search_api_index.remove_field_ajax', $route_parameters), + ], ); } } diff --git a/src/Form/UnsavedConfigurationFormTrait.php b/src/Form/UnsavedConfigurationFormTrait.php index 15b4e57e..6f7d27e1 100644 --- a/src/Form/UnsavedConfigurationFormTrait.php +++ b/src/Form/UnsavedConfigurationFormTrait.php @@ -97,18 +97,7 @@ protected function checkEntityEditable(array &$form, $entity, $reportChanged = F ); } elseif ($reportChanged) { - $form['changed'] = array( - '#type' => 'container', - '#attributes' => array( - 'class' => array( - 'index-changed', - 'messages', - 'messages--warning', - ), - ), - '#children' => $this->t('You have unsaved changes.'), - '#weight' => -10, - ); + drupal_set_message($this->t('You have unsaved changes.'), 'warning'); } } } diff --git a/tests/src/Functional/IntegrationTest.php b/tests/src/Functional/IntegrationTest.php index 3bfbd64f..32cb1507 100644 --- a/tests/src/Functional/IntegrationTest.php +++ b/tests/src/Functional/IntegrationTest.php @@ -1035,7 +1035,7 @@ protected function checkUnsavedChanges() { $this->assertSession()->responseContains($message_parts[0]); $this->assertSession()->responseContains($message_parts[1]); $this->assertFalse($this->xpath('//input[not(@disabled)]')); - $this->drupalGet($this->getIndexPath('fields/rendered_item/edit')); + $this->drupalGet($this->getIndexPath('fields/edit/rendered_item')); $this->assertSession()->responseContains($message_parts[0]); $this->assertSession()->responseContains($message_parts[1]); $this->assertFalse($this->xpath('//input[not(@disabled)]')); diff --git a/tests/src/Functional/ProcessorIntegrationTest.php b/tests/src/Functional/ProcessorIntegrationTest.php index a8cdb021..1208b73e 100644 --- a/tests/src/Functional/ProcessorIntegrationTest.php +++ b/tests/src/Functional/ProcessorIntegrationTest.php @@ -307,7 +307,7 @@ public function checkAggregatedFieldsIntegration() { $this->submitForm([], 'aggregated_field'); $args['%label'] = 'Aggregated field'; $this->assertSession()->responseContains(new FormattableMarkup('Field %label was added to the index.', $args)); - $this->assertSession()->addressEquals($this->getIndexPath('fields/aggregated_field/edit')); + $this->assertSession()->addressEquals($this->getIndexPath('fields/edit/aggregated_field')); $edit = [ 'type' => 'first', 'fields[entity:node/title]' => 'title', @@ -500,7 +500,7 @@ public function checkRenderedItemIntegration() { $this->submitForm([], 'rendered_item'); $args['%label'] = 'Rendered HTML output'; $this->assertSession()->responseContains(new FormattableMarkup('Field %label was added to the index.', $args)); - $this->assertSession()->addressEquals($this->getIndexPath('fields/rendered_item/edit')); + $this->assertSession()->addressEquals($this->getIndexPath('fields/edit/rendered_item')); $edit = [ 'roles[]' => ['authenticated'], 'view_mode[entity:node][article]' => 'default',