Index: modules/node/node.admin.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/node/node.admin.inc,v
retrieving revision 1.74
diff -u -r1.74 node.admin.inc
--- modules/node/node.admin.inc	27 Oct 2009 04:06:44 -0000	1.74
+++ modules/node/node.admin.inc	2 Nov 2009 14:39:42 -0000
@@ -70,8 +70,22 @@
  */
 function node_filters() {
   // Regular filters
+  $filters['titlesearch'] = array(
+    'title' => t('title'),
+    'type' => 'textfield',
+  );
+
+  // If the search module is enabled, use it to search the contents of nodes.
+  if (module_exists('search')) {
+    $filters['textsearch'] = array(
+      'title' => t('data'),
+      'type' => 'textfield',
+    );
+  }
+
   $filters['status'] = array(
     'title' => t('status'),
+    'type' => 'select',
     'options' => array(
       'status-1' => t('published'),
       'status-0' => t('not published'),
@@ -89,16 +103,16 @@
     );
   }
 
-  $filters['type'] = array('title' => t('type'), 'options' => node_type_get_names());
+  $filters['type'] = array('title' => t('type'), 'type' => 'select', 'options' => node_type_get_names());
 
   // The taxonomy filter
   if ($taxonomy = module_invoke('taxonomy', 'form_all', 1)) {
-    $filters['term'] = array('title' => t('term'), 'options' => $taxonomy);
+    $filters['term'] = array('title' => t('term'), 'type' => 'select', 'options' => $taxonomy);
   }
   // Language filter if there is a list of languages
   if ($languages = module_invoke('locale', 'language_list')) {
     $languages = array('' => t('Language neutral')) + $languages;
-    $filters['language'] = array('title' => t('language'), 'options' => $languages);
+    $filters['language'] = array('title' => t('language'), 'type' => 'select', 'options' => $languages);
   }
   return $filters;
 }
@@ -116,6 +130,33 @@
   foreach ($filter_data as $index => $filter) {
     list($key, $value) = $filter;
     switch ($key) {
+      case 'titlesearch':
+        $query->condition('n.title', '%' . $value .'%', 'LIKE');
+        break;
+      case 'textsearch':
+        // Avoid errors when there is session data but the module is disabled.
+        if (!module_exists('search')) {
+          break;
+        }
+
+        // First get a list of nids that match the description.
+        $result = node_search_execute($value);
+        $nids = array();
+        if (is_array($result)) {
+          foreach ($result as $node_result) {
+            $nids[] = $node_result['node']->nid;
+          }
+        }
+
+        if (!empty($nids)) {
+          // Now restrict to just that list of nids.
+          $query->condition('nid', $nids);
+        }
+        else {
+          // No nodes match, force a return of nothing without an error.
+          $query->condition('1', '0');
+        }
+      break;
       case 'term':
         $index = 'tn' . $counter++;
         $query->join('taxonomy_term_node', $index, "n.nid = $index.nid");
@@ -155,7 +196,7 @@
     elseif ($type == 'language') {
       $value = empty($value) ? t('Language neutral') : module_invoke('locale', 'language_name', $value);
     }
-    else {
+    elseif ($filters[$type]['type'] == 'select') {
       $value = $filters[$type]['options'][$value];
     }
     if ($i++) {
@@ -164,18 +205,21 @@
     else {
       $form['filters']['current'][] = array('#markup' => t('<strong>%type</strong> is <strong>%value</strong>', array('%type' => $filters[$type]['title'], '%value' => $value)));
     }
-    if (in_array($type, array('type', 'language'))) {
-      // Remove the option if it is already being filtered on.
+    if (in_array($type, array('type', 'language')) || $filters[$type]['type'] == 'textfield') {
+      // Remove the option if it is already being filtered on and can't have multiple options.
       unset($filters[$type]);
     }
   }
 
   foreach ($filters as $key => $filter) {
     $names[$key] = $filter['title'];
-    $form['filters']['status'][$key] = array('#type' => 'select', '#options' => $filter['options']);
+    $form['filters']['status'][$key] = array('#type' => $filter['type']);
+    if (isset($filter['options'])) {
+      $form['filters']['status'][$key]['#options'] = $filter['options'];
+    }
   }
 
-  $form['filters']['filter'] = array('#type' => 'radios', '#options' => $names, '#default_value' => 'status');
+  $form['filters']['filter'] = array('#type' => 'checkboxes', '#options' => $names);
   $form['filters']['buttons']['submit'] = array('#type' => 'submit', '#value' => (count($session) ? t('Refine') : t('Filter')));
   if (count($session)) {
     $form['filters']['buttons']['undo'] = array('#type' => 'submit', '#value' => t('Undo'));
@@ -247,14 +291,26 @@
   switch ($form_state['values']['op']) {
     case t('Filter'):
     case t('Refine'):
-      if (isset($form_state['values']['filter'])) {
-        $filter = $form_state['values']['filter'];
-
-        // Flatten the options array to accommodate hierarchical/nested options.
-        $flat_options = form_options_flatten($filters[$filter]['options']);
+      foreach ($form_state['values']['filter'] as $filter) {
+        // Filter may not have been checked, but will still be in the array.
+        if (empty($filter)) {
+          continue;
+        }
 
-        if (isset($flat_options[$form_state['values'][$filter]])) {
-          $_SESSION['node_overview_filter'][] = array($filter, $form_state['values'][$filter]);
+        if ($filters[$filter]['type'] == 'select' && is_array($filters[$filter]['options'])) {
+          // Flatten the options array to accommodate hierarchical/nested options.
+          $flat_options = form_options_flatten($filters[$filter]['options']);
+
+          if (isset($flat_options[$form_state['values'][$filter]])) {
+            $_SESSION['node_overview_filter'][] = array($filter, $form_state['values'][$filter]);
+          }
+        }
+        else {
+          // Text fields.
+          $value = $form_state['values'][$filter];
+          if (!empty($value)) {
+            $_SESSION['node_overview_filter'][] = array($filter, $value);
+          }
         }
       }
       break;
Index: modules/node/node.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/node/node.test,v
retrieving revision 1.51
diff -u -r1.51 node.test
--- modules/node/node.test	30 Oct 2009 22:33:35 -0000	1.51
+++ modules/node/node.test	2 Nov 2009 14:39:42 -0000
@@ -704,7 +704,8 @@
     // Enable dummy module that implements hook_node_grants(),
     // hook_node_access_records(), hook_node_grants_alter() and
     // hook_node_access_records_alter().
-    parent::setUp('node_test');
+    // Search module is an optional dependency, used in the node search form.
+    parent::setUp('node_test', 'search');
   }
 
   /**
@@ -973,6 +974,8 @@
     $node1 = $this->drupalCreateNode(array('type' => 'article', 'status' => 1));
     $node2 = $this->drupalCreateNode(array('type' => 'article', 'status' => 0));
     $node3 = $this->drupalCreateNode(array('type' => 'page'));
+    // Additional node for title search tests.
+    $node4 = $this->drupalCreateNode(array('type' => 'article', 'status' => 1));
 
     $this->drupalGet('admin/content');
     $this->assertText($node1->title[FIELD_LANGUAGE_NONE][0]['value'], t('Node appears on the node administration listing.'));
@@ -982,24 +985,78 @@
 
     // Filter the node listing by status.
     $edit = array(
-      'filter' => 'status',
+      'filter[status]' => 'status',
       'status' => 'status-1',
     );
     $this->drupalPost('admin/content', $edit, t('Filter'));
     $this->assertRaw(t('<strong>%type</strong> is <strong>%value</strong>', array('%type' => t('status'), '%value' => t('published'))), t('The node administration listing is filtered by status.'));
-    $this->assertText($node1->title[FIELD_LANGUAGE_NONE][0]['value'], t('Published node appears on the node administration listing.'));
+    // For result assertRaw, must appear in the node list as a link rather than in the filter.
+    $this->assertRaw($node1->title[FIELD_LANGUAGE_NONE][0]['value'] . '</a>', t('Published node appears on the node administration listing.'));
+    // NoText = shouldn't appear at all.
     $this->assertNoText($node2->title[FIELD_LANGUAGE_NONE][0]['value'], t('Unpublished node does not appear on the node administration listing.'));
 
+    $this->verbose($node1->title[FIELD_LANGUAGE_NONE][0]['value'] . '</a>');
+
     // Filter the node listing by content type.
     $edit = array(
-      'filter' => 'type',
+      'filter[type]' => 'type',
       'type' => 'article',
     );
     $this->drupalPost('admin/content', $edit, t('Refine'));
     $this->assertRaw(t('<strong>%type</strong> is <strong>%value</strong>', array('%type' => t('status'), '%value' => t('published'))), t('The node administration listing is filtered by status.'));
     $this->assertRaw(t('<strong>%type</strong> is <strong>%value</strong>', array('%type' => t('type'), '%value' => 'Article')), t('The node administration listing is filtered by content type.'));
-    $this->assertText($node1->title[FIELD_LANGUAGE_NONE][0]['value'], t('Article node appears on the node administration listing.'));
+    $this->assertRaw($node1->title[FIELD_LANGUAGE_NONE][0]['value'] . '</a>', t('Article node appears on the node administration listing.'));
+    $this->assertNoText($node3->title[FIELD_LANGUAGE_NONE][0]['value'], t('Page node does not appear on the node administration listing.'));
+
+    // Filter the node listing by title.
+    $edit = array(
+      'filter[titlesearch]' => 'titlesearch',
+      'titlesearch' => $node1->title[FIELD_LANGUAGE_NONE][0]['value'],
+    );
+    $this->drupalPost('admin/content', $edit, t('Refine'));
+    $this->assertRaw(t('<strong>%type</strong> is <strong>%value</strong>', array('%type' => t('status'), '%value' => t('published'))), t('The node administration listing is filtered by status.'));
+    $this->assertRaw(t('<strong>%type</strong> is <strong>%value</strong>', array('%type' => t('type'), '%value' => 'Article')), t('The node administration listing is filtered by content type.'));
+    $this->assertRaw(t('<strong>%type</strong> is <strong>%value</strong>', array('%type' => t('title'), '%value' => $node1->title[FIELD_LANGUAGE_NONE][0]['value'])), t('The node administration listing is filtered by node title.'));
+    $this->assertRaw($node1->title[FIELD_LANGUAGE_NONE][0]['value'] . '</a>', t('Article node appears on the node administration listing.'));
+    $this->assertNoText($node3->title[FIELD_LANGUAGE_NONE][0]['value'], t('Page node does not appear on the node administration listing.'));
+    $this->assertNoText($node4->title[FIELD_LANGUAGE_NONE][0]['value'], t('Non-matching article title does not appear on the node administration listing.'));
+
+    // Test the Undo button, which removes the most recently added filter.
+    $edit = array();
+    $this->drupalPost('admin/content', $edit, t('Undo'));
+    $this->assertRaw(t('<strong>%type</strong> is <strong>%value</strong>', array('%type' => t('status'), '%value' => t('published'))), t('The node administration listing is filtered by status.'));
+    $this->assertRaw(t('<strong>%type</strong> is <strong>%value</strong>', array('%type' => t('type'), '%value' => 'Article')), t('The node administration listing is filtered by content type.'));
+    $this->assertNoRaw(t('<strong>%type</strong> is <strong>%value</strong>', array('%type' => t('title'), '%value' => $node1->title[FIELD_LANGUAGE_NONE][0]['value'])), t('The node administration listing is no longer filtered by title.'));
+    $this->assertRaw($node1->title[FIELD_LANGUAGE_NONE][0]['value'] . '</a>', t('Article node appears on the node administration listing.'));
     $this->assertNoText($node3->title[FIELD_LANGUAGE_NONE][0]['value'], t('Page node does not appear on the node administration listing.'));
+    $this->assertRaw($node4->title[FIELD_LANGUAGE_NONE][0]['value'] . '</a>', t('Non-matching article title now appears again on the node administration listing.'));
+
+    // Test the Reset button, which removes all remaining filters.
+    $edit = array();
+    $this->drupalPost('admin/content', $edit, t('Reset'));
+    $this->assertNoRaw(t('<strong>%type</strong> is <strong>%value</strong>', array('%type' => t('status'), '%value' => t('published'))), t('The node administration listing is no longer filtered by status.'));
+    $this->assertNoRaw(t('<strong>%type</strong> is <strong>%value</strong>', array('%type' => t('type'), '%value' => 'Article')), t('The node administration listing is no longer filtered by content type.'));
+    $this->assertNoRaw(t('<strong>%type</strong> is <strong>%value</strong>', array('%type' => t('title'), '%value' => $node1->title[FIELD_LANGUAGE_NONE][0]['value'])), t('The node administration listing is no longer filtered by title.'));
+    $this->assertRaw($node1->title[FIELD_LANGUAGE_NONE][0]['value'] . '</a>', t('Article node appears on the node administration listing.'));
+    $this->assertRaw($node3->title[FIELD_LANGUAGE_NONE][0]['value'] . '</a>', t('Page node appears on the node administration listing.'));
+    $this->assertRaw($node4->title[FIELD_LANGUAGE_NONE][0]['value'] . '</a>', t('Non-matching article title now appears again on the node administration listing.'));
+
+    // Filter the node listing by search data.  Must trigger search indexing first.
+    /* TODO: The textsearch field needs search index data in place for it to work.
+    $edit = array(
+      'filter[textsearch]' => 'textsearch',
+      'textsearch' => $node2->title[FIELD_LANGUAGE_NONE][0]['value'],
+    );
+    $this->drupalPost('admin/content', $edit, t('Filter'));
+    $this->assertNoRaw(t('<strong>%type</strong> is <strong>%value</strong>', array('%type' => t('status'), '%value' => t('published'))), t('The node administration listing is no longer filtered by status.'));
+    $this->assertNoRaw(t('<strong>%type</strong> is <strong>%value</strong>', array('%type' => t('type'), '%value' => 'Article')), t('The node administration listing is no longer filtered by content type.'));
+    $this->assertNoRaw(t('<strong>%type</strong> is <strong>%value</strong>', array('%type' => t('title'), '%value' => $node1->title[FIELD_LANGUAGE_NONE][0]['value'])), t('The node administration listing is no longer filtered by title.'));
+    $this->assertRaw(t('<strong>%type</strong> is <strong>%value</strong>', array('%type' => t('data'), '%value' => $node2->title[FIELD_LANGUAGE_NONE][0]['value'])), t('The node administration listing filtered by search data.'));
+    $this->assertRaw($node2->title[FIELD_LANGUAGE_NONE][0]['value'] . '</a>', t('Node matching search appears on the node administration listing.'));
+    $this->assertNoText($node1->title[FIELD_LANGUAGE_NONE][0]['value'], t('Non-matching article title does not appear on the node administration listing.'));
+    */
+
+    // TODO: Also test language and taxonomy filters.
   }
 }
 
Index: modules/system/system.css
===================================================================
RCS file: /cvs/drupal/drupal/modules/system/system.css,v
retrieving revision 1.63
diff -u -r1.63 system.css
--- modules/system/system.css	21 Sep 2009 08:52:41 -0000	1.63
+++ modules/system/system.css	2 Nov 2009 14:39:42 -0000
@@ -193,7 +193,7 @@
   padding-bottom: 0;
   font-size: 0.9em;
 }
-dl.multiselect dd.b, dl.multiselect dd.b .form-item, dl.multiselect dd.b select {
+dl.multiselect dd.b, dl.multiselect dd.b .form-item, dl.multiselect dd.b select, dl.multiselect dd.b input.form-text {
   font-family: inherit;
   font-size: inherit;
   width: 14em;
Index: misc/form.js
===================================================================
RCS file: /cvs/drupal/drupal/misc/form.js,v
retrieving revision 1.12
diff -u -r1.12 form.js
--- misc/form.js	16 Oct 2009 16:37:00 -0000	1.12
+++ misc/form.js	2 Nov 2009 14:39:41 -0000
@@ -61,9 +61,9 @@
 Drupal.behaviors.multiselectSelector = {
   attach: function (context, settings) {
     // Automatically selects the right radio button in a multiselect control.
-    $('.multiselect select', context).once('multiselect').change(function () {
-        $('.multiselect input:radio[value="' + this.id.substr(5) + '"]')
-          .attr('checked', true);
+    $('.multiselect select, .multiselect input:text', context).once('multiselect').change(function () {
+      $('.multiselect input:radio[value="' + this.name + '"], .multiselect input:checkbox[value="' + this.name + '"]')
+        .attr('checked', true);
     });
   }
 };
