diff options
author | Lauri Eskola <lauri.eskola@acquia.com> | 2023-04-20 19:09:36 +0300 |
---|---|---|
committer | Lauri Eskola <lauri.eskola@acquia.com> | 2023-04-20 19:10:52 +0300 |
commit | 1996c9d44d336255c9788ed26b7f8cf24656601c (patch) | |
tree | 9fe5b857ec048f86d32435358f844e93c54687ff | |
parent | d7bd8ac42006fb5eb4c68acf5b99cc2a6a79d496 (diff) | |
download | drupal-1996c9d44d336255c9788ed26b7f8cf24656601c.tar.gz drupal-1996c9d44d336255c9788ed26b7f8cf24656601c.zip |
Issue #857312 by bnjmnm, tim.plunkett, yched, swentel, nod_, nick_schuch, smustgrave: Add a "changes not applied until saved" warning when changing widget/formatter settings
-rw-r--r-- | core/core.libraries.yml | 8 | ||||
-rw-r--r-- | core/lib/Drupal/Core/Ajax/TabledragWarningCommand.php | 53 | ||||
-rw-r--r-- | core/misc/tabledrag-ajax.js | 36 | ||||
-rw-r--r-- | core/misc/tabledrag.js | 50 | ||||
-rw-r--r-- | core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5AllowedTagsTest.php | 1 | ||||
-rw-r--r-- | core/modules/field_ui/field_ui.js | 60 | ||||
-rw-r--r-- | core/modules/field_ui/src/Form/EntityDisplayFormBase.php | 22 | ||||
-rw-r--r-- | core/modules/field_ui/tests/src/Functional/EntityDisplayFormBaseTest.php | 73 | ||||
-rw-r--r-- | core/modules/field_ui/tests/src/FunctionalJavascript/ManageDisplayTest.php | 81 | ||||
-rw-r--r-- | core/phpstan-baseline.neon | 2 | ||||
-rw-r--r-- | core/themes/claro/js/tabledrag.js | 1 |
11 files changed, 360 insertions, 27 deletions
diff --git a/core/core.libraries.yml b/core/core.libraries.yml index 01909a756e3..06579085cf9 100644 --- a/core/core.libraries.yml +++ b/core/core.libraries.yml @@ -678,6 +678,14 @@ drupal.tabledrag: - core/once - core/drupal.touchevents-test +drupal.tabledrag.ajax: + version: VERSION + js: + misc/tabledrag-ajax.js: { } + dependencies: + - core/ajax + - core/tabledrag + drupal.tableheader: version: VERSION js: diff --git a/core/lib/Drupal/Core/Ajax/TabledragWarningCommand.php b/core/lib/Drupal/Core/Ajax/TabledragWarningCommand.php new file mode 100644 index 00000000000..e1cc60892e3 --- /dev/null +++ b/core/lib/Drupal/Core/Ajax/TabledragWarningCommand.php @@ -0,0 +1,53 @@ +<?php + +namespace Drupal\Core\Ajax; + +use Drupal\Core\Asset\AttachedAssets; + +/** + * AJAX command for conveying changed tabledrag rows. + * + * This command is provided an id of a table row then does the following: + * - Marks the row as changed. + * - If a message generated by the tableDragChangedWarning is not present above + * the table the row belongs to, that message is added there. + * + * @see Drupal.AjaxCommands.prototype.tabledragChanged + * + * @ingroup ajax + */ +class TabledragWarningCommand implements CommandInterface, CommandWithAttachedAssetsInterface { + + /** + * Constructs a TableDragWarningCommand object. + * + * @param string $id + * The id of the changed row. + * @param string $tabledrag_instance + * The identifier of the tabledrag instance. + */ + public function __construct( + protected string $id, + protected string $tabledrag_instance) {} + + /** + * {@inheritdoc} + */ + public function render() { + return [ + 'command' => 'tabledragChanged', + 'id' => $this->id, + 'tabledrag_instance' => $this->tabledrag_instance, + ]; + } + + /** + * {@inheritdoc} + */ + public function getAttachedAssets() { + $assets = new AttachedAssets(); + $assets->setLibraries(['core/drupal.tabledrag.ajax']); + return $assets; + } + +} diff --git a/core/misc/tabledrag-ajax.js b/core/misc/tabledrag-ajax.js new file mode 100644 index 00000000000..11df818dd88 --- /dev/null +++ b/core/misc/tabledrag-ajax.js @@ -0,0 +1,36 @@ +/** + * Ajax command for highlighting elements. + * + * @param {Drupal.Ajax} [ajax] + * An Ajax object. + * @param {object} response + * The Ajax response. + * @param {string} response.id + * The row id. + * @param {string} response.tabledrag_instance + * The tabledrag instance identifier. + * @param {number} [status] + * The HTTP status code. + */ +Drupal.AjaxCommands.prototype.tabledragChanged = function ( + ajax, + response, + status, +) { + if (status !== 'success') { + return; + } + + const tableDrag = Drupal.tableDrag[response.tabledrag_instance]; + + // eslint-disable-next-line new-cap + const rowObject = new tableDrag.row( + document.getElementById(response.id), + '', + tableDrag.indentEnabled, + tableDrag.maxDepth, + true, + ); + rowObject.markChanged(); + rowObject.addChangedWarning(); +}; diff --git a/core/misc/tabledrag.js b/core/misc/tabledrag.js index ea05dc8215e..bd25008347f 100644 --- a/core/misc/tabledrag.js +++ b/core/misc/tabledrag.js @@ -180,6 +180,14 @@ * @type {boolean} */ this.indentEnabled = false; + + /** + * Keeps track of rows that have changed. + */ + this.changedRowIds = Drupal.tableDrag[table.id] + ? Drupal.tableDrag[table.id].changedRowIds + : new Set(); + Object.keys(tableSettings || {}).forEach((group) => { Object.keys(tableSettings[group] || {}).forEach((n) => { if (tableSettings[group][n].relationship === 'parent') { @@ -269,6 +277,20 @@ } }, this), ); + + // Check for any rows marked as changed before this tabledrag was rerendered + // and mark them as changed for this current render. + this.changedRowIds.forEach((changedRowId) => { + // eslint-disable-next-line new-cap + const rowObject = new self.row( + document.getElementById(changedRowId), + '', + self.indentEnabled, + self.maxDepth, + true, + ); + rowObject.markChanged(); + }); }; /** @@ -842,10 +864,7 @@ self.rowObject.markChanged(); if (self.changed === false) { - $(Drupal.theme('tableDragChangedWarning')) - .insertBefore(self.table) - .hide() - .fadeIn('slow'); + self.rowObject.addChangedWarning(); self.changed = true; } } @@ -1335,6 +1354,28 @@ }; /** + * Adds a warning above the table informing users they must save changes. + */ + Drupal.tableDrag.prototype.row.prototype.addChangedWarning = function () { + // Do not add the changed warning if one is already present. + if (!$(this.table.parentNode).find('.tabledrag-changed-warning').length) { + const $form = $(this.table).closest('form'); + $(Drupal.theme('tableDragChangedWarning')) + .insertBefore(this.table) + .hide() + // If a warning has already been shown, do not fade the warning in, so + // it appears static when the table is rebuilt. + .fadeIn( + $form[0].hasAttribute('data-tabledrag-save-warning') ? 0 : 'slow', + ); + + // Keep track of the warning having been added in an element that lives + // outside the table which rebuilds when certain changes occur. + $form[0].setAttribute('data-tabledrag-save-warning', true); + } + }; + + /** * Find all children of rowObject by indentation. * * @param {boolean} addClasses @@ -1619,6 +1660,7 @@ if (cell.find('abbr.tabledrag-changed').length === 0) { cell.append(marker); } + Drupal.tableDrag[this.table.id].changedRowIds.add(this.element.id); }; /** diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5AllowedTagsTest.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5AllowedTagsTest.php index a20df5ee56b..60ccc9e85fc 100644 --- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5AllowedTagsTest.php +++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5AllowedTagsTest.php @@ -382,6 +382,7 @@ class CKEditor5AllowedTagsTest extends CKEditor5TestBase { $this->assertNotNull($assert_session->waitForElementVisible('css', '[data-drupal-selector=edit-filters-media-embed-settings]', 0)); $page->clickLink('Embed media'); + $assert_session->waitForField('filters[media_embed][settings][allowed_view_modes][view_mode_2]'); $page->checkField('filters[media_embed][settings][allowed_view_modes][view_mode_1]'); $page->checkField('filters[media_embed][settings][allowed_view_modes][view_mode_2]'); $assert_session->assertWaitOnAjaxRequest(); diff --git a/core/modules/field_ui/field_ui.js b/core/modules/field_ui/field_ui.js index d6e2b5c55e5..a381226dfb1 100644 --- a/core/modules/field_ui/field_ui.js +++ b/core/modules/field_ui/field_ui.js @@ -149,8 +149,14 @@ onChange() { const $trigger = $(this); const $row = $trigger.closest('tr'); - const rowHandler = $row.data('fieldUIRowHandler'); + // Do not fire change listeners for items within forms that have their + // own AJAX callbacks to process a change. + if ($trigger.closest('.ajax-new-content').length !== 0) { + return; + } + + const rowHandler = $row.data('fieldUIRowHandler'); const refreshRows = {}; refreshRows[rowHandler.name] = $trigger.get(0); @@ -168,8 +174,27 @@ rowHandler.region = region; } - // Ajax-update the rows. - Drupal.fieldUIOverview.AJAXRefreshRows(refreshRows); + // Fields inside `.tabledrag-hide` are typically hidden. They can be + // visible when "Show row weights" are enabled. If their value is changed + // while visible, the row should be marked as changed, but they should not + // be processed via AJAXRefreshRows as they are intended to be fields AJAX + // updates the value of. + if ($trigger.closest('.tabledrag-hide').length) { + const thisTableDrag = Drupal.tableDrag['field-display-overview']; + // eslint-disable-next-line new-cap + const rowObject = new thisTableDrag.row( + $row[0], + '', + thisTableDrag.indentEnabled, + thisTableDrag.maxDepth, + true, + ); + rowObject.markChanged(); + rowObject.addChangedWarning(); + } else { + // Ajax-update the rows. + Drupal.fieldUIOverview.AJAXRefreshRows(refreshRows); + } }, /** @@ -262,7 +287,6 @@ rowNames.push(rowName); ajaxElements.push(rows[rowName]); }); - if (rowNames.length) { // Add a throbber next each of the ajaxElements. $(ajaxElements).after(Drupal.theme.ajaxProgressThrobber()); @@ -285,9 +309,11 @@ // jQuery trigger(). $(input).on('mousedown', () => { returnFocus = { - drupalSelector: document.activeElement.getAttribute( + drupalSelector: document.activeElement.hasAttribute( 'data-drupal-selector', - ), + ) + ? document.activeElement.getAttribute('data-drupal-selector') + : false, scrollY: window.scrollY, }; }); @@ -300,14 +326,13 @@ `[data-drupal-selector="${returnFocus.drupalSelector}"]`, ) .focus(); - - // Ensure the scroll position is the same as when the input was - // initially changed. - window.scrollTo({ - top: returnFocus.scrollY, - }); - returnFocus = {}; } + // Ensure the scroll position is the same as when the input was + // initially changed. + window.scrollTo({ + top: returnFocus.scrollY, + }); + returnFocus = {}; }); }); $('input[data-drupal-selector="edit-refresh"]').trigger('mousedown'); @@ -347,14 +372,11 @@ this.region = data.region; this.tableDrag = data.tableDrag; this.defaultPlugin = data.defaultPlugin; - - // Attach change listener to the 'plugin type' select. this.$pluginSelect = $(row).find('.field-plugin-type'); - this.$pluginSelect.on('change', Drupal.fieldUIOverview.onChange); - - // Attach change listener to the 'region' select. this.$regionSelect = $(row).find('select.field-region'); - this.$regionSelect.on('change', Drupal.fieldUIOverview.onChange); + + // Attach change listeners to select and input elements in the row. + $(row).find('select, input').on('change', Drupal.fieldUIOverview.onChange); return this; }; diff --git a/core/modules/field_ui/src/Form/EntityDisplayFormBase.php b/core/modules/field_ui/src/Form/EntityDisplayFormBase.php index 6cc3f6bd058..a92618a728b 100644 --- a/core/modules/field_ui/src/Form/EntityDisplayFormBase.php +++ b/core/modules/field_ui/src/Form/EntityDisplayFormBase.php @@ -4,6 +4,10 @@ namespace Drupal\field_ui\Form; use Drupal\Component\Plugin\Factory\DefaultFactory; use Drupal\Component\Plugin\PluginManagerBase; +use Drupal\Component\Utility\Html; +use Drupal\Core\Ajax\AjaxResponse; +use Drupal\Core\Ajax\ReplaceCommand; +use Drupal\Core\Ajax\TabledragWarningCommand; use Drupal\Core\Entity\EntityFieldManagerInterface; use Drupal\Core\Entity\EntityForm; use Drupal\Core\Entity\EntityInterface; @@ -299,7 +303,7 @@ abstract class EntityDisplayFormBase extends EntityForm { // Disable fields without any applicable plugins. if (empty($this->getApplicablePluginOptions($field_definition))) { - $this->entity->removeComponent($field_name)->save(); + $this->entity->removeComponent($field_name); $display_options = $this->entity->getComponent($field_name); } @@ -679,6 +683,7 @@ abstract class EntityDisplayFormBase extends EntityForm { * Ajax handler for multistep buttons. */ public function multistepAjax($form, FormStateInterface $form_state) { + $response = new AjaxResponse(); $trigger = $form_state->getTriggeringElement(); $op = $trigger['#op']; @@ -709,8 +714,19 @@ abstract class EntityDisplayFormBase extends EntityForm { } } - // Return the whole table. - return $form['fields']; + // Replace the whole table. + $response->addCommand(new ReplaceCommand('#field-display-overview-wrapper', $form['fields'])); + + // Add "row updated" warning after the table has been replaced. + if (!in_array($op, ['cancel', 'edit'])) { + foreach ($updated_rows as $name) { + // The ID of the rendered table row is `$name` processed by getClass(). + // @see \Drupal\field_ui\Element\FieldUiTable::tablePreRender + $response->addCommand(new TabledragWarningCommand(Html::getClass($name), 'field-display-overview')); + } + } + + return $response; } /** diff --git a/core/modules/field_ui/tests/src/Functional/EntityDisplayFormBaseTest.php b/core/modules/field_ui/tests/src/Functional/EntityDisplayFormBaseTest.php new file mode 100644 index 00000000000..a2fb83175b3 --- /dev/null +++ b/core/modules/field_ui/tests/src/Functional/EntityDisplayFormBaseTest.php @@ -0,0 +1,73 @@ +<?php + +namespace Drupal\Tests\field_ui\Functional; + +use Drupal\field\Entity\FieldConfig; +use Drupal\field\Entity\FieldStorageConfig; +use Drupal\Tests\BrowserTestBase; + +/** + * Tests the UI for configuring entity displays. + * + * @group field_ui + */ +class EntityDisplayFormBaseTest extends BrowserTestBase { + + /** + * {@inheritdoc} + */ + protected static $modules = ['field_ui', 'entity_test', 'field_test']; + + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + + foreach (entity_test_entity_types() as $entity_type) { + // Auto-create fields for testing. + FieldStorageConfig::create([ + 'entity_type' => $entity_type, + 'field_name' => 'field_test_no_plugin', + 'type' => 'field_test', + 'cardinality' => 1, + ])->save(); + FieldConfig::create([ + 'entity_type' => $entity_type, + 'field_name' => 'field_test_no_plugin', + 'bundle' => $entity_type, + 'label' => 'Test field with no plugin', + 'translatable' => FALSE, + ])->save(); + + \Drupal::service('entity_display.repository') + ->getFormDisplay($entity_type, $entity_type) + ->setComponent('field_test_no_plugin', []) + ->save(); + } + + $this->drupalLogin($this->drupalCreateUser([ + 'administer entity_test form display', + ])); + } + + /** + * Ensures the entity is not affected when there are no applicable formatters. + */ + public function testNoApplicableFormatters(): void { + $storage = $this->container->get('entity_type.manager')->getStorage('entity_form_display'); + $id = 'entity_test.entity_test.default'; + + $entity_before = $storage->load($id); + $this->drupalGet('entity_test/structure/entity_test/form-display'); + $entity_after = $storage->load($id); + + $this->assertSame($entity_before->toArray(), $entity_after->toArray()); + } + +} diff --git a/core/modules/field_ui/tests/src/FunctionalJavascript/ManageDisplayTest.php b/core/modules/field_ui/tests/src/FunctionalJavascript/ManageDisplayTest.php index 03ed77eb7a2..fbc61854948 100644 --- a/core/modules/field_ui/tests/src/FunctionalJavascript/ManageDisplayTest.php +++ b/core/modules/field_ui/tests/src/FunctionalJavascript/ManageDisplayTest.php @@ -485,4 +485,85 @@ class ManageDisplayTest extends WebDriverTestBase { $this->assertNotEmpty($row, 'Field was created and appears in the overview page.'); } + /** + * Confirms that notifications to save appear when necessary. + */ + public function testNotAppliedUntilSavedWarning() { + $assert_session = $this->assertSession(); + $page = $this->getSession()->getPage(); + + // Admin Manage Fields page. + $manage_fields = 'admin/structure/types/manage/' . $this->type; + + $this->fieldUIAddNewField($manage_fields, 'test', 'Test field'); + $manage_display = 'admin/structure/types/manage/' . $this->type . '/display'; + $manage_form = 'admin/structure/types/manage/' . $this->type . '/form-display'; + + // Form display, change widget type. + $this->drupalGet($manage_form); + $assert_session->elementNotExists('css', '.tabledrag-changed-warning'); + $assert_session->elementNotExists('css', 'abbr.tabledrag-changed'); + $page->selectFieldOption('fields[uid][type]', 'options_buttons'); + $this->assertNotNull($changed_warning = $assert_session->waitForElementVisible('css', '.tabledrag-changed-warning')); + $this->assertNotNull($assert_session->waitForElementVisible('css', ' #uid abbr.tabledrag-changed')); + $this->assertSame('* You have unsaved changes.', $changed_warning->getText()); + + // Form display, change widget settings. + $this->drupalGet($manage_form); + $edit_widget_button = $assert_session->waitForElementVisible('css', '[data-drupal-selector="edit-fields-uid-settings-edit"]'); + $edit_widget_button->press(); + $assert_session->waitForText('3rd party formatter settings form'); + + // Confirm the AJAX operation of opening the form does not result in the row + // being set as changed. New settings must be submitted for that to happen. + $assert_session->elementNotExists('css', 'abbr.tabledrag-changed'); + $cancel_button = $assert_session->waitForElementVisible('css', '[data-drupal-selector="edit-fields-uid-settings-edit-form-actions-cancel-settings"]'); + $cancel_button->press(); + $assert_session->assertNoElementAfterWait('css', '[data-drupal-selector="edit-fields-uid-settings-edit-form-actions-cancel-settings"]'); + $assert_session->elementNotExists('css', '.tabledrag-changed-warning'); + $assert_session->elementNotExists('css', 'abbr.tabledrag-changed'); + $edit_widget_button = $assert_session->waitForElementVisible('css', '[data-drupal-selector="edit-fields-uid-settings-edit"]'); + $edit_widget_button->press(); + $widget_field = $assert_session->waitForField('fields[uid][settings_edit_form][third_party_settings][field_third_party_test][field_test_widget_third_party_settings_form]'); + $widget_field->setValue('honk'); + $update_button = $assert_session->waitForElementVisible('css', '[data-drupal-selector="edit-fields-uid-settings-edit-form-actions-save-settings"]'); + $update_button->press(); + $assert_session->assertNoElementAfterWait('css', '[data-drupal-selector="edit-fields-field-test-settings-edit-form-actions-cancel-settings"]'); + $this->assertNotNull($changed_warning = $assert_session->waitForElementVisible('css', '.tabledrag-changed-warning')); + $this->assertNotNull($assert_session->waitForElementVisible('css', ' #uid abbr.tabledrag-changed')); + $this->assertSame('* You have unsaved changes.', $changed_warning->getText()); + + // Content display, change formatter type. + $this->drupalGet($manage_display); + $assert_session->elementNotExists('css', '.tabledrag-changed-warning'); + $assert_session->elementNotExists('css', 'abbr.tabledrag-changed'); + $page->selectFieldOption('edit-fields-field-test-label', 'inline'); + $this->assertNotNull($changed_warning = $assert_session->waitForElementVisible('css', '.tabledrag-changed-warning')); + $this->assertNotNull($assert_session->waitForElementVisible('css', ' #field-test abbr.tabledrag-changed')); + $this->assertSame('* You have unsaved changes.', $changed_warning->getText()); + + // Content display, change formatter settings. + $this->drupalGet($manage_display); + $assert_session->elementNotExists('css', '.tabledrag-changed-warning'); + $assert_session->elementNotExists('css', 'abbr.tabledrag-changed'); + $edit_formatter_button = $assert_session->waitForElementVisible('css', '[data-drupal-selector="edit-fields-field-test-settings-edit"]'); + $edit_formatter_button->press(); + $assert_session->waitForText('3rd party formatter settings form'); + $cancel_button = $assert_session->waitForElementVisible('css', '[data-drupal-selector="edit-fields-field-test-settings-edit-form-actions-cancel-settings"]'); + $cancel_button->press(); + $assert_session->assertNoElementAfterWait('css', '[data-drupal-selector="edit-fields-field-test-settings-edit-form-actions-cancel-settings"]'); + $assert_session->elementNotExists('css', '.tabledrag-changed-warning'); + $assert_session->elementNotExists('css', 'abbr.tabledrag-changed'); + $edit_formatter_button = $assert_session->waitForElementVisible('css', '[data-drupal-selector="edit-fields-field-test-settings-edit"]'); + $edit_formatter_button->press(); + $formatter_field = $assert_session->waitForField('fields[field_test][settings_edit_form][third_party_settings][field_third_party_test][field_test_field_formatter_third_party_settings_form]'); + $formatter_field->setValue('honk'); + $update_button = $assert_session->waitForElementVisible('css', '[data-drupal-selector="edit-fields-field-test-settings-edit-form-actions-save-settings"]'); + $update_button->press(); + $assert_session->assertNoElementAfterWait('css', '[data-drupal-selector="edit-fields-field-test-settings-edit-form-actions-cancel-settings"]'); + $this->assertNotNull($changed_warning = $assert_session->waitForElementVisible('css', '.tabledrag-changed-warning')); + $this->assertNotNull($assert_session->waitForElementVisible('css', ' #field-test abbr.tabledrag-changed')); + $this->assertSame('* You have unsaved changes.', $changed_warning->getText()); + } + } diff --git a/core/phpstan-baseline.neon b/core/phpstan-baseline.neon index efb5e397649..a09976ba7d1 100644 --- a/core/phpstan-baseline.neon +++ b/core/phpstan-baseline.neon @@ -1217,7 +1217,7 @@ parameters: - message: "#^Variable \\$updated_rows might not be defined\\.$#" - count: 1 + count: 2 path: modules/field_ui/src/Form/EntityDisplayFormBase.php - diff --git a/core/themes/claro/js/tabledrag.js b/core/themes/claro/js/tabledrag.js index b6a9d331116..79df748de15 100644 --- a/core/themes/claro/js/tabledrag.js +++ b/core/themes/claro/js/tabledrag.js @@ -122,6 +122,7 @@ if (cell.find('.js-tabledrag-changed-marker').length === 0) { cell.find('.js-tabledrag-handle').after(marker); } + Drupal.tableDrag[this.table.id].changedRowIds.add(this.element.id); }, /** |