diff --git a/core/modules/update/src/Form/UpdateManagerUpdate.php b/core/modules/update/src/Form/UpdateManagerUpdate.php
index fb5385c0260..7b2b680ed6b 100644
--- a/core/modules/update/src/Form/UpdateManagerUpdate.php
+++ b/core/modules/update/src/Form/UpdateManagerUpdate.php
@@ -199,42 +199,49 @@ class UpdateManagerUpdate extends FormBase {
// Drupal core needs to be upgraded manually.
$needs_manual = $project['project_type'] == 'core';
+ // If the recommended release for a contributed project is not compatible
+ // with the currently installed version of core, list that project in a
+ // separate table. To determine if the release is compatible, we inspect
+ // the 'core_compatible' key from the release info array. If it's not
+ // defined, it means we can't determine compatibility requirements (or
+ // we're looking at core), so we assume it is compatible.
+ $compatible = $recommended_release['core_compatible'] ?? TRUE;
+
if ($needs_manual) {
- // There are no checkboxes in the 'Manual updates' table so it will be
- // rendered by '#theme' => 'table', not '#theme' => 'tableselect'. Since
- // the data formats are incompatible, we convert now to the format
- // expected by '#theme' => 'table'.
- unset($entry['#weight']);
- $attributes = $entry['#attributes'];
- unset($entry['#attributes']);
- $entry = [
- 'data' => $entry,
- ] + $attributes;
+ $this->removeCheckboxFromRow($entry);
+ $projects['manual'][$name] = $entry;
+ }
+ elseif (!$compatible) {
+ $this->removeCheckboxFromRow($entry);
+ // If the release has a core_compatibility_message, inject it.
+ if (!empty($recommended_release['core_compatibility_message'])) {
+ // @todo In https://www.drupal.org/project/drupal/issues/3121769
+ // refactor this into something theme-friendly so we don't have a
+ // classless
';
+ $entry['data']['recommended_version']['data']['#context']['core_compatibility_message'] = $recommended_release['core_compatibility_message'];
+ }
+ $projects['not-compatible'][$name] = $entry;
}
else {
$form['project_downloads'][$name] = [
'#type' => 'value',
'#value' => $recommended_release['download_link'],
];
- }
- // Based on what kind of project this is, save the entry into the
- // appropriate subarray.
- switch ($project['project_type']) {
- case 'core':
- // Core needs manual updates at this time.
- $projects['manual'][$name] = $entry;
- break;
+ // Based on what kind of project this is, save the entry into the
+ // appropriate subarray.
+ switch ($project['project_type']) {
+ case 'module':
+ case 'theme':
+ $projects['enabled'][$name] = $entry;
+ break;
- case 'module':
- case 'theme':
- $projects['enabled'][$name] = $entry;
- break;
-
- case 'module-disabled':
- case 'theme-disabled':
- $projects['disabled'][$name] = $entry;
- break;
+ case 'module-disabled':
+ case 'theme-disabled':
+ $projects['disabled'][$name] = $entry;
+ break;
+ }
}
}
@@ -297,9 +304,45 @@ class UpdateManagerUpdate extends FormBase {
];
}
+ if (!empty($projects['not-compatible'])) {
+ $form['not_compatible'] = [
+ '#type' => 'table',
+ '#header' => $headers,
+ '#rows' => $projects['not-compatible'],
+ '#prefix' => '
' . $this->t('Not compatible') . '
',
+ '#weight' => 150,
+ ];
+ }
+
return $form;
}
+ /**
+ * Prepares a row entry for use in a regular table, not a 'tableselect'.
+ *
+ * There are no checkboxes in the 'Manual updates' or 'Not compatible' tables,
+ * so they will be rendered by '#theme' => 'table', not 'tableselect'. Since
+ * the data formats are incompatible, this method converts to the format
+ * expected by '#theme' => 'table'. Generally, rows end up in the main tables
+ * that have a checkbox to allow the site admin to select which missing
+ * updates to install. This method is only used for the special case tables
+ * that have no such checkbox.
+ *
+ * @todo In https://www.drupal.org/project/drupal/issues/3121775 refactor
+ * self::buildForm() so that we don't need this method at all.
+ *
+ * @param array[] $row
+ * The render array for a table row.
+ */
+ protected function removeCheckboxFromRow(array &$row) {
+ unset($row['#weight']);
+ $attributes = $row['#attributes'];
+ unset($row['#attributes']);
+ $row = [
+ 'data' => $row,
+ ] + $attributes;
+ }
+
/**
* {@inheritdoc}
*/
diff --git a/core/modules/update/tests/modules/update_test/bbb_update_test.1_1.xml b/core/modules/update/tests/modules/update_test/bbb_update_test.1_1.xml
new file mode 100644
index 00000000000..9bac7ba1f61
--- /dev/null
+++ b/core/modules/update/tests/modules/update_test/bbb_update_test.1_1.xml
@@ -0,0 +1,48 @@
+
+
+
+BBB Update test
+bbb_update_test
+Drupal
+8.x-1.
+published
+http://example.com/project/bbb_update_test
+
+ ProjectsModules
+
+
+
+ bbb_update_test 8.x-1.1
+ 8.x-1.1
+ 8.x-1.1
+ published
+ http://example.com/bbb_update_test-8-x-1-1-release
+ http://example.com/bbb_update_test-8.x-1.1.tar.gz
+ 1250444521
+
+ Release typeBug fixes
+
+
+
+ bbb_update_test 8.x-1.0
+ 8.x-1.0
+ 8.x-1.0
+ published
+ http://example.com/bbb_update_test-8-x-1-0-release
+ http://example.com/bbb_update_test-8.x-1.0.tar.gz
+ 1250424521
+
+ Release typeNew features
+ Release typeBug fixes
+
+
+
+
diff --git a/core/modules/update/tests/modules/update_test/bbb_update_test.1_2.xml b/core/modules/update/tests/modules/update_test/bbb_update_test.1_2.xml
new file mode 100644
index 00000000000..f15467e689c
--- /dev/null
+++ b/core/modules/update/tests/modules/update_test/bbb_update_test.1_2.xml
@@ -0,0 +1,62 @@
+
+
+
+BBB Update test
+bbb_update_test
+Drupal
+8.x-1.
+published
+http://example.com/project/bbb_update_test
+
+ ProjectsModules
+
+
+
+ bbb_update_test 8.x-1.2
+ 8.x-1.2
+ 8.x-1.2
+ ^8.1.0
+ published
+ http://example.com/bbb_update_test-8-x-1-2-release
+ http://example.com/bbb_update_test-8.x-1.2.tar.gz
+ 1250445521
+
+ Release typeBug fixes
+
+
+
+ bbb_update_test 8.x-1.1
+ 8.x-1.1
+ 8.x-1.1
+ published
+ http://example.com/bbb_update_test-8-x-1-1-release
+ http://example.com/bbb_update_test-8.x-1.1.tar.gz
+ 1250444521
+
+ Release typeBug fixes
+
+
+
+ bbb_update_test 8.x-1.0
+ 8.x-1.0
+ 8.x-1.0
+ published
+ http://example.com/bbb_update_test-8-x-1-0-release
+ http://example.com/bbb_update_test-8.x-1.0.tar.gz
+ 1250424521
+
+ Release typeNew features
+ Release typeBug fixes
+
+
+
+
diff --git a/core/modules/update/tests/src/Functional/UpdateManagerUpdateTest.php b/core/modules/update/tests/src/Functional/UpdateManagerUpdateTest.php
new file mode 100644
index 00000000000..3e87e9a651c
--- /dev/null
+++ b/core/modules/update/tests/src/Functional/UpdateManagerUpdateTest.php
@@ -0,0 +1,236 @@
+drupalCreateUser([
+ 'administer software updates',
+ 'administer site configuration',
+ ]);
+ $this->drupalLogin($admin_user);
+
+ // The installed state of the system is the same for all test cases. What
+ // varies for each test scenario is which release history fixture we fetch,
+ // which in turn changes the expected state of the UpdateManagerUpdateForm.
+ $system_info = [
+ '#all' => [
+ 'version' => '8.0.0',
+ ],
+ 'aaa_update_test' => [
+ 'project' => 'aaa_update_test',
+ 'version' => '8.x-1.0',
+ 'hidden' => FALSE,
+ ],
+ 'bbb_update_test' => [
+ 'project' => 'bbb_update_test',
+ 'version' => '8.x-1.0',
+ 'hidden' => FALSE,
+ ],
+ ];
+ $this->config('update_test.settings')->set('system_info', $system_info)->save();
+ }
+
+ /**
+ * Provides data for test scenarios involving incompatible updates.
+ *
+ * These test cases rely on the following fixtures containing the following
+ * releases:
+ * - aaa_update_test.8.x-1.2.xml
+ * - 8.x-1.2 Compatible with 8.0.0 core.
+ * - aaa_update_test.core_compatibility.8.x-1.2_8.x-2.2.xml
+ * - 8.x-1.2 Requires 8.1.0 and above.
+ * - bbb_update_test.1_0.xml
+ * - 8.x-1.0 is the only available release.
+ * - bbb_update_test.1_1.xml
+ * - 8.x-1.1 is available and compatible with everything (does not define
+ * at all).
+ * - bbb_update_test.1_2.xml
+ * - 8.x-1.1 is available and compatible with everything (does not define
+ * at all).
+ * - 8.x-1.2 is available and requires Drupal 8.1.0 and above.
+ *
+ * @todo In https://www.drupal.org/project/drupal/issues/3112962:
+ * Change the 'core_fixture' values here to use:
+ * - '1.1' instead of '1.1-core_compatibility'.
+ * - '1.1-alpha1' instead of '1.1-alpha1-core_compatibility'.
+ * Delete the files:
+ * - core/modules/update/tests/modules/update_test/drupal.1.1-alpha1-core_compatibility.xml
+ * - core/modules/update/tests/modules/update_test/drupal.1.1-core_compatibility.xml
+ *
+ * @return array[]
+ * Test data.
+ */
+ public function incompatibleUpdatesTableProvider() {
+ return [
+ 'only one compatible' => [
+ 'core_fixture' => '1.1-core_compatibility',
+ // aaa_update_test.8.x-1.2.xml has core compatibility set and will test
+ // the case where $recommended_release['core_compatible'] === TRUE in
+ // \Drupal\update\Form\UpdateManagerUpdate.
+ 'a_fixture' => '8.x-1.2',
+ // Use a fixture with only a 8.x-1.0 release so BBB is up to date.
+ 'b_fixture' => '1_0',
+ 'compatible' => [
+ 'AAA' => '8.x-1.2',
+ ],
+ 'incompatible' => [],
+ ],
+ 'only one incompatible' => [
+ 'core_fixture' => '1.1-core_compatibility',
+ 'a_fixture' => 'core_compatibility.8.x-1.2_8.x-2.2',
+ // Use a fixture with only a 8.x-1.0 release so BBB is up to date.
+ 'b_fixture' => '1_0',
+ 'compatible' => [],
+ 'incompatible' => [
+ 'AAA' => [
+ 'recommended' => '8.x-1.2',
+ 'range' => '8.1.0 to 8.1.1',
+ ],
+ ],
+ ],
+ 'two compatible, no incompatible' => [
+ 'core_fixture' => '1.1-core_compatibility',
+ 'a_fixture' => '8.x-1.2',
+ // bbb_update_test.1_1.xml does not have core compatibility set and will
+ // test the case where $recommended_release['core_compatible'] === NULL
+ // in \Drupal\update\Form\UpdateManagerUpdate.
+ 'b_fixture' => '1_1',
+ 'compatible' => [
+ 'AAA' => '8.x-1.2',
+ 'BBB' => '8.x-1.1',
+ ],
+ 'incompatible' => [],
+ ],
+ 'two incompatible, no compatible' => [
+ 'core_fixture' => '1.1-core_compatibility',
+ 'a_fixture' => 'core_compatibility.8.x-1.2_8.x-2.2',
+ // bbb_update_test.1_2.xml has core compatibility set and will test the
+ // case where $recommended_release['core_compatible'] === FALSE in
+ // \Drupal\update\Form\UpdateManagerUpdate.
+ 'b_fixture' => '1_2',
+ 'compatible' => [],
+ 'incompatible' => [
+ 'AAA' => [
+ 'recommended' => '8.x-1.2',
+ 'range' => '8.1.0 to 8.1.1',
+ ],
+ 'BBB' => [
+ 'recommended' => '8.x-1.2',
+ 'range' => '8.1.0 to 8.1.1',
+ ],
+ ],
+ ],
+ 'one compatible, one incompatible' => [
+ 'core_fixture' => '1.1-core_compatibility',
+ 'a_fixture' => 'core_compatibility.8.x-1.2_8.x-2.2',
+ 'b_fixture' => '1_1',
+ 'compatible' => [
+ 'BBB' => '8.x-1.1',
+ ],
+ 'incompatible' => [
+ 'AAA' => [
+ 'recommended' => '8.x-1.2',
+ 'range' => '8.1.0 to 8.1.1',
+ ],
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * Tests the Update form for a single test scenario of incompatible updates.
+ *
+ * @dataProvider incompatibleUpdatesTableProvider
+ *
+ * @param string $core_fixture
+ * The fixture file to use for Drupal core.
+ * @param string $a_fixture
+ * The fixture file to use for the aaa_update_test module.
+ * @param string $b_fixture
+ * The fixture file to use for the bbb_update_test module.
+ * @param string[] $compatible
+ * Compatible recommended updates (if any). Keys are module identifier
+ * ('AAA' or 'BBB') and values are the expected recommended release.
+ * @param string[][] $incompatible
+ * Incompatible recommended updates (if any). Keys are module identifier
+ * ('AAA' or 'BBB') and values are subarrays with the following keys:
+ * - 'recommended': The recommended version.
+ * - 'range': The versions of Drupal core required for that version.
+ */
+ public function testIncompatibleUpdatesTable($core_fixture, $a_fixture, $b_fixture, array $compatible, array $incompatible) {
+
+ $assert_session = $this->assertSession();
+ $compatible_table_locator = '[data-drupal-selector="edit-projects"]';
+ $incompatible_table_locator = '[data-drupal-selector="edit-not-compatible"]';
+
+ $this->refreshUpdateStatus(['drupal' => $core_fixture, 'aaa_update_test' => $a_fixture, 'bbb_update_test' => $b_fixture]);
+ $this->drupalGet('admin/reports/updates/update');
+
+ if ($compatible) {
+ // Verify the number of rows in the table.
+ $assert_session->elementsCount('css', "$compatible_table_locator tbody tr", count($compatible));
+ // We never want to see a compatibility range in the compatible table.
+ $assert_session->elementTextNotContains('css', $compatible_table_locator, 'Requires Drupal core');
+ foreach ($compatible as $module => $version) {
+ $compatible_row = "$compatible_table_locator tbody tr:contains('$module Update test')";
+ // First
is the checkbox, so start with td #2.
+ $assert_session->elementTextContains('css', "$compatible_row td:nth-of-type(2)", "$module Update test");
+ // Both contrib modules use 8.x-1.0 as the currently installed version.
+ $assert_session->elementTextContains('css', "$compatible_row td:nth-of-type(3)", '8.x-1.0');
+ $assert_session->elementTextContains('css', "$compatible_row td:nth-of-type(4)", $version);
+ }
+ }
+ else {
+ // Verify there is no compatible updates table.
+ $assert_session->elementNotExists('css', $compatible_table_locator);
+ }
+
+ if ($incompatible) {
+ // Verify the number of rows in the table.
+ $assert_session->elementsCount('css', "$incompatible_table_locator tbody tr", count($incompatible));
+ foreach ($incompatible as $module => $data) {
+ $incompatible_row = "$incompatible_table_locator tbody tr:contains('$module Update test')";
+ $assert_session->elementTextContains('css', "$incompatible_row td:nth-of-type(1)", "$module Update test");
+ // Both contrib modules use 8.x-1.0 as the currently installed version.
+ $assert_session->elementTextContains('css', "$incompatible_row td:nth-of-type(2)", '8.x-1.0');
+ $assert_session->elementTextContains('css', "$incompatible_row td:nth-of-type(3)", $data['recommended']);
+ $assert_session->elementTextContains('css', "$incompatible_row td:nth-of-type(3)", 'Requires Drupal core: ' . $data['range']);
+ }
+ }
+ else {
+ // Verify there is no incompatible updates table.
+ $assert_session->elementNotExists('css', $incompatible_table_locator);
+ }
+ }
+
+}