Преглед на файлове

Merge branch '7.x-2.x' of git.drupal.org:project/tripal into 7.x-2.x

Stephen Ficklin преди 10 години
родител
ревизия
a0487aeba3

+ 199 - 0
tripal_core/api/tripal_core.chado_nodes.api.inc

@@ -133,6 +133,205 @@ function chado_node_get_base_table($content_type, $module = FALSE) {
 
 }
 
+/** 
+ * @section
+ * Common Functionality for Properties, Dbxrefs and relationships chado node API
+ */
+
+/**
+ * Validate the Triggering element from a node form.
+ * 
+ * We are going to inspect the post to determine what PHP knows is the triggering
+ * element and if it doesn't agree with Drupal then we are actually going to
+ * change it in Drupal.
+ * 
+ * This fixes an obscure bug triggered when a property is added and then
+ * a relationship removed, Drupal thinks the first property remove button was
+ * clicked and instead removes a property (not a relationship) and renders the new
+ * property table in the relationship table page space.
+ * 
+ * NOTE: Many Drupal issues state that this problem is solved if the #name
+ * of the button is unique (which it is in our case) but we are still experiencing
+ * incorrectly determined triggering elements so we need to handle it ourselves.
+ */
+function chado_validate_node_form_triggering_element($form, &$form_state) {
+  
+  // We are going to inspect the post to determine what PHP knows is the triggering
+  // element and if it doesn't agree with Drupal then we are actually going to
+  // change it in Drupal.
+  if ($_POST['_triggering_element_name'] != $form_state['triggering_element']['#name']) {
+    $form_state['triggering_element']['#name'] = $_POST['_triggering_element_name'];
+  }
+  
+}
+
+/**
+ * Validate Adding Subtables entries from the node forms.
+ * Supported subtables: Properties, Relationships, Additional DBxrefs.
+ *
+ * @param array $form
+ * @param array $form_state
+ */
+function chado_add_node_form_subtables_add_button_validate($form, &$form_state) {
+
+  // Based on triggering element call the correct validation function
+  // ASUMPTION #1: each of the buttons must have property, dbxref or relationship
+  // as the first part of the #name to uniquely identify the subsection.
+  if (preg_match('/^([a-z]+).*/', $form_state['triggering_element']['#name'], $matches)) {
+    $subsection = $matches[1];
+
+    switch($subsection) {
+      case 'properties':
+        chado_add_node_form_properties_add_button_validate($form, $form_state);
+        break;
+      case 'dbxrefs':
+        chado_add_node_form_dbxrefs_add_button_validate($form, $form_state);
+        break;
+      case 'relationships':
+        chado_add_node_form_relationships_add_button_validate($form, $form_state);
+        break;
+    }
+  }
+}
+
+/**
+ * Add subtable entries to the node forms.
+ * Supported subtables: Properties, Relationships, Additional DBxrefs.
+ *
+ * @param array $form
+ * @param array $form_state
+ */
+function chado_add_node_form_subtables_add_button_submit($form, &$form_state) {
+
+  // Based on triggering element call the correct submit function
+  // ASUMPTION #1: each of the buttons must have properties, dbxrefs or relationships
+  // as the first part of the #name to uniquely identify the subsection.
+  if (preg_match('/^([a-z]+).*/', $form_state['triggering_element']['#name'], $matches)) {
+    $subsection = $matches[1];
+
+     switch($subsection) {
+      case 'properties':
+        chado_add_node_form_properties_add_button_submit($form, $form_state);
+        break;
+      case 'dbxrefs':
+        chado_add_node_form_dbxrefs_add_button_submit($form, $form_state);
+        break;
+      case 'relationships':
+        chado_add_node_form_relationships_add_button_submit($form, $form_state);
+        break;
+    }
+  }
+  
+  // This is needed to ensure the form builder function is called for the node
+  // form in order for any of these changes to be seen.
+  $form_state['rebuild'] = TRUE;
+}
+
+/**
+ * Validate Removing Subtables entries from the node forms.
+ * Supported subtables: Properties, Relationships, Additional DBxrefs.
+ * 
+ * Since Removing isn't associated with any user input the only thing we
+ * need to validate is that Drupal has determined the triggering element correctly.
+ * That said, we will call each subtables associated validate function just incase
+ * there is some case-specific validation we do not know of or have not anticipated.
+ * 
+ * @param array $form
+ * @param array $form_state
+ */
+function chado_add_node_form_subtables_remove_button_validate($form, &$form_state) {
+ 
+  // We need to validate the trigerring element since Drupal has known
+  // issues determining this correctly when there are multiple buttons
+  // with the same label.
+  chado_validate_node_form_triggering_element($form, $form_state);
+  
+  // Based on triggering element call the correct validation function
+  // ASUMPTION #1: each of the buttons must have property, dbxref or relationship
+  // as the first part of the #name to uniquely identify the subsection.
+  if (preg_match('/^([a-z]+).*/', $form_state['triggering_element']['#name'], $matches)) {
+    $subsection = $matches[1];
+    
+      switch($subsection) {
+      case 'properties':
+        chado_add_node_form_properties_remove_button_validate($form, $form_state);
+        break;
+      case 'dbxrefs':
+        chado_add_node_form_dbxrefs_remove_button_validate($form, $form_state);
+        break;
+      case 'relationships':
+        chado_add_node_form_relationships_remove_button_validate($form, $form_state);
+        break;
+    }
+  }
+}
+
+/**
+ * Remove subtable entries to the node forms.
+ * Supported subtables: Properties, Relationships, Additional DBxrefs.
+ *
+ * @param array $form
+ * @param array $form_state
+ */
+function chado_add_node_form_subtables_remove_button_submit($form, &$form_state) {
+
+  // Based on triggering element call the correct submit function
+  // ASUMPTION #1: each of the buttons must have properties, dbxrefs or relationships
+  // as the first part of the #name to uniquely identify the subsection.
+  if (preg_match('/^([a-z]+).*/', $form_state['triggering_element']['#name'], $matches)) {
+    $subsection = $matches[1];
+
+    switch($subsection) {
+      case 'properties':
+        chado_add_node_form_properties_remove_button_submit($form, $form_state);
+        break;
+      case 'dbxrefs':
+        chado_add_node_form_dbxrefs_remove_button_submit($form, $form_state);
+        break;
+      case 'relationships':
+        chado_add_node_form_relationships_remove_button_submit($form, $form_state);
+        break;
+    }
+  }
+
+  // This is needed to ensure the form builder function is called for the node
+  // form in order for any of these changes to be seen.
+  $form_state['rebuild'] = TRUE;
+}
+
+/**
+ * Ajax function which returns the section of the form to be re-rendered
+ * for either the properties, dbxref or relationship sub-sections.
+ *
+ * @ingroup tripal_core
+ */
+function chado_add_node_form_subtable_ajax_update($form, &$form_state) {
+  
+  // We need to validate the trigerring element since Drupal has known
+  // issues determining this correctly when there are multiple buttons
+  // with the same label.
+  chado_validate_node_form_triggering_element($form, $form_state);
+  
+  // Based on triggering element render the correct part of the form.
+  // ASUMPTION: each of the buttons must have property, dbxref or relationship
+  // as the first part of the #name to uniquely identify the subsection.
+  if (preg_match('/^([a-z]+).*/', $form_state['triggering_element']['#name'], $matches)) {
+    $subsection = $matches[1];
+    
+    switch($subsection) {
+      case 'properties':
+        return $form['properties']['property_table'];
+        break;
+      case 'dbxrefs':
+        return $form['addtl_dbxrefs']['dbxref_table'];
+        break;
+      case 'relationships':
+        return $form['relationships']['relationship_table'];
+        break;
+    }
+  }
+}
+
 /**
  * @section
  * Sync Form

+ 118 - 79
tripal_core/api/tripal_core.chado_nodes.dbxrefs.api.inc

@@ -125,7 +125,7 @@
 function chado_add_node_form_dbxrefs(&$form, &$form_state, $details) {
 
   // Set defaults for optional fields
-  $details['fieldset_title'] = 'Additional Database References';
+  $details['fieldset_title'] = 'External References';
   $details['additional_instructions'] = '';
 
   // Get the list of databases to display in the dropdown.
@@ -142,6 +142,9 @@ function chado_add_node_form_dbxrefs(&$form, &$form_state, $details) {
       $db_options[$db->db_id] = $db->name;
     }
   }
+  
+  // Determine the node type using the name of the foreign key.
+  $details['nodetype'] = str_replace('_id', '', $details['base_foreign_key']);
 
   // Tell tripal administrators how to add terms to the property types drop down.
   $importance = (empty($db_options)) ? TRIPAL_WARNING : TRIPAL_INFO;
@@ -156,22 +159,40 @@ function chado_add_node_form_dbxrefs(&$form, &$form_state, $details) {
     array('return_html' => TRUE)
   );
 
+  // Group all of the chado node api fieldsets into vertical tabs.
+  $form['chado_node_api'] = array(
+    '#type' => 'vertical_tabs',
+    '#attached' => array(
+      'css' => array(
+        'chado-node-api' => drupal_get_path('module', 'tripal_core') . '/theme/css/chado_node_api.css',
+      ),
+    ),
+  );
+
   // the fieldset of the dbxref elements
+  $instructions = 'To add an external reference, select the database you want to reference from the 
+    drop-down below. Then enter the name/accession (as it is shown in the external database) of this 
+    particular %nodetype into the text box before clicking "Add". The version can be used to 
+    indicate the version of the external database or the version of the reference 
+    depending upon what is available. To remove incorrect references, click the 
+    "Remove" button. Note: you cannot edit previously added references but instead 
+    need to remove and re-add them.';
   $form['addtl_dbxrefs'] = array(
     '#type' => 'fieldset',
     '#title' => t($details['fieldset_title']),
-    '#description' => t('You may add additional database references by
-      selecting a database reference type from the dropdown and adding text.  You may add
-      as many database references as desired by clicking the add button on the right.  To
-      remove a database reference, click the remove button. ' . $details['additional_instructions']),
-    '#prefix' => "<div id='addtl-dbxrefs-fieldset'>",
-    '#suffix' => '</div>',
-    '#weight'      => 9
-  );
-
-  $form['addtl_dbxrefs']['admin_message'] = array(
-    '#type' => 'markup',
-    '#markup' => $tripal_message
+    '#description' => t('<p><strong>Indicate that this %nodetype either originates from 
+      or is present in another database.</strong></p><p>'. $instructions . $details['additional_instructions'] . '</p>', 
+      array('%nodetype' => $details['nodetype'])),
+    '#collapsible' => TRUE,
+    '#collapsed' => TRUE,
+    '#group' => 'chado_node_api',
+    '#weight'      => 9,
+    '#attributes' => array('class' => array('chado-node-api','dbxrefs')),
+    '#attached' => array(
+      'js' => array(
+        'chado-node-api-vertical-tabs' => drupal_get_path('module', 'tripal_core') . '/theme/js/chadoNodeApi_updateVerticalTabSummary.js',
+      ),
+    ),
   );
 
   // this form element is a tree, so that we don't puke all of the values into then node variable
@@ -184,6 +205,20 @@ function chado_add_node_form_dbxrefs(&$form, &$form_state, $details) {
     '#theme' => 'chado_node_additional_dbxrefs_form_table'
   );
 
+  // We need to provide feedback to the user that changes made
+  // are not saved until the node is saved.
+  $form['addtl_dbxrefs']['dbxref_table']['save_warning'] = array(
+    '#type' => 'markup',
+    '#prefix' => '<div id="dbxref-save-warning" class="messages warning" style="display:none;">',
+    '#suffix' => '</div>',
+    '#markup' => '* The changes to these references will not be saved until the "Save" button at the bottom of this form is clicked. <span class="specific-changes"></span>',
+    '#attached' => array(
+      'js' => array(
+        'chado-node-api-unsaved' => drupal_get_path('module', 'tripal_core') . '/theme/js/chadoNodeApi_unsavedNotify.js',
+      ),
+    ),
+  );
+
   /* DBxrefs can come to us in two ways:
    *
    * 1) In the form state in the $form_state['chado_additional_dbxrefs']. Data is in this field
@@ -200,9 +235,12 @@ function chado_add_node_form_dbxrefs(&$form, &$form_state, $details) {
   else {
     $existing_dbxrefs = chado_query(
       "SELECT
-         db.name as db_name, db.db_id as db_id,
-         dbxref.dbxref_id, dbxref.accession as accession,
-         dbxref.description as description, dbxref.version
+         db.name as db_name, 
+         db.db_id as db_id,
+         dbxref.dbxref_id, 
+         dbxref.accession as accession,
+         dbxref.description as description, 
+         dbxref.version
        FROM {dbxref} dbxref
          LEFT JOIN {db} db ON db.db_id = dbxref.db_id
          LEFT JOIN {".$details['linking_table']."} linking_table ON linking_table.dbxref_id = dbxref.dbxref_id
@@ -218,7 +256,7 @@ function chado_add_node_form_dbxrefs(&$form, &$form_state, $details) {
    *   '[db_id]-[version]' => array(
    *     'db_id' => [the db.db_id value]
    *     'db_name' => [the db.name value]
-   *     'dbxref_id' => [the dbxref.dbxref_id value, or NULL if it doesn't yet exists],
+   *     'dbxref_id' => [the dbxref.dbxref_id value, or temporary value if it doesn't yet exists],
    *     'version' => [the dbxref.version value],
    *     'accession' => [the dbxref.accession value],
    *   ),
@@ -242,65 +280,71 @@ function chado_add_node_form_dbxrefs(&$form, &$form_state, $details) {
    * Loop on the array elements of the $existing_dbxrefs array and add
    * an element to the form for each one.
    */
+  $num_dbxrefs = 0;
   foreach ($existing_dbxrefs as $dbxref) {
-
     if (array_key_exists($dbxref->db_id, $db_options)) {
+      $num_dbxrefs++;
 
-      /* Since the dbxref version is part of the unique constraint, when it is
-       * empty we need to use something in the key to indicate this case. Otherwise,
-       * you wouldn't be able to select those elements from the array
-       * (ie: $form['addtl_dbxrefs']['dbxref_table'][9999][''] doesn't work as
-       * expected whereas $form['addtl_dbxrefs']['dbxref_table'][9999][NONE]
-       * is much better)
-       */
-      $version = (!empty($dbxref->version)) ? $dbxref->version : 'NONE';
+      $form['addtl_dbxrefs']['dbxref_table'][$dbxref->db_id]['#type'] = 'markup';
+      $form['addtl_dbxrefs']['dbxref_table'][$dbxref->db_id]['#value'] = '';
 
-      $form['addtl_dbxrefs']['dbxref_table'][$dbxref->db_id] = array(
-        '#type' => 'markup',
-        '#value' => ''
+      $form['addtl_dbxrefs']['dbxref_table'][$dbxref->db_id][$dbxref->dbxref_id]['#type'] = 'markup';
+      $form['addtl_dbxrefs']['dbxref_table'][$dbxref->db_id][$dbxref->dbxref_id]['#value'] = '';
+      $form['addtl_dbxrefs']['dbxref_table'][$dbxref->db_id][$dbxref->dbxref_id]['#attributes'] = array(
+        'class' => array('dbxref', 'saved')
       );
 
-      $form['addtl_dbxrefs']['dbxref_table'][$dbxref->db_id][$version] = array(
-        '#type' => 'markup',
-        '#value' => ''
-      );
+      // Determine whether this dbxref is unsaved or not.
+      // We can tell this by looking at the dbxref_id: if it's not
+      // saved yet we will have entered a TEMP###.
+      if (preg_match('/^TEMP/', $dbxref->dbxref_id)) {
+        $form['addtl_dbxrefs']['dbxref_table'][$dbxref->db_id][$dbxref->dbxref_id]['#attributes'] = array(
+          'class' => array('dbxref', 'unsaved')
+        );
+      }
 
-      $form['addtl_dbxrefs']['dbxref_table'][$dbxref->db_id][$version]['db_id'] = array(
+      $form['addtl_dbxrefs']['dbxref_table'][$dbxref->db_id][$dbxref->dbxref_id]['db_id'] = array(
         '#type' => 'hidden',
         '#value' => $dbxref->db_id
       );
 
-      $form['addtl_dbxrefs']['dbxref_table'][$dbxref->db_id][$version]['accession'] = array(
+      $form['addtl_dbxrefs']['dbxref_table'][$dbxref->db_id][$dbxref->dbxref_id]['accession'] = array(
         '#type' => 'hidden',
         '#value' => $dbxref->accession
       );
 
-      $form['addtl_dbxrefs']['dbxref_table'][$dbxref->db_id][$version]['dbxref_id'] = array(
+      $form['addtl_dbxrefs']['dbxref_table'][$dbxref->db_id][$dbxref->dbxref_id]['version'] = array(
+        '#type' => 'hidden',
+        '#value' => $dbxref->version,
+      );
+
+      $form['addtl_dbxrefs']['dbxref_table'][$dbxref->db_id][$dbxref->dbxref_id]['dbxref_id'] = array(
         '#type' => 'hidden',
         '#value' => $dbxref->dbxref_id
       );
 
-      $form['addtl_dbxrefs']['dbxref_table'][$dbxref->db_id][$version]['db'] = array(
+      $form['addtl_dbxrefs']['dbxref_table'][$dbxref->db_id][$dbxref->dbxref_id]['db'] = array(
         '#type' => 'markup',
-        '#markup' => $dbxref->db_name
+        '#markup' => $dbxref->db_name,
+        '#prefix' => '<span class="row-unsaved-warning"></span>'
       );
 
-      $form['addtl_dbxrefs']['dbxref_table'][$dbxref->db_id][$version]['dbxref_version'] = array(
+      $form['addtl_dbxrefs']['dbxref_table'][$dbxref->db_id][$dbxref->dbxref_id]['dbxref_version'] = array(
         '#type' => 'markup',
         '#markup' => $dbxref->version,
       );
 
-      $form['addtl_dbxrefs']['dbxref_table'][$dbxref->db_id][$version]['dbxref_accession'] = array(
+      $form['addtl_dbxrefs']['dbxref_table'][$dbxref->db_id][$dbxref->dbxref_id]['dbxref_accession'] = array(
         '#type' => 'markup',
         '#markup' => $dbxref->accession
       );
       // remove button
-      $form['addtl_dbxrefs']['dbxref_table'][$dbxref->db_id][$version]['dbxref_action'] = array(
+      $form['addtl_dbxrefs']['dbxref_table'][$dbxref->db_id][$dbxref->dbxref_id]['dbxref_action'] = array(
         '#type' => 'submit',
         '#value' => t('Remove'),
-        '#name' => "dbxref_remove-".$dbxref->db_id.'-'.$version,
+        '#name' => "dbxrefs_remove-".$dbxref->db_id.'-'.$dbxref->dbxref_id,
         '#ajax' => array(
-          'callback' => "chado_add_node_form_dbxrefs_ajax_update",
+          'callback' => "chado_add_node_form_subtable_ajax_update",
           'wrapper' => 'tripal-generic-edit-addtl_dbxrefs-table',
           'effect'   => 'fade',
           'method'   => 'replace',
@@ -313,8 +357,8 @@ function chado_add_node_form_dbxrefs(&$form, &$form_state, $details) {
         // from the chado_additional_dbxrefs array. In order to keep validate errors
         // from the node form validate and Drupal required errors for non-dbxref fields
         // preventing the user from removing dbxrefs we set the #limit_validation_errors below
-        '#validate' => array('chado_add_node_form_dbxrefs_remove_button_validate'),
-        '#submit' => array('chado_add_node_form_dbxrefs_remove_button_submit'),
+        '#validate' => array('chado_add_node_form_subtables_remove_button_validate'),
+        '#submit' => array('chado_add_node_form_subtables_remove_button_submit'),
         // Limit the validation of the form upon clicking this button to the dbxref_table tree
         // No other fields will be validated (ie: no fields from the main form or any other api
         // added form).
@@ -325,6 +369,13 @@ function chado_add_node_form_dbxrefs(&$form, &$form_state, $details) {
     }
   }
 
+  // Quickly add a hidden field stating how many dbxrefs are currently added.
+  $form['addtl_dbxrefs']['num_dbxrefs'] = array(
+    '#type' => 'hidden',
+    '#value' => $num_dbxrefs,
+    '#attributes' => array('class' => 'num-dbxrefs')
+  );
+
   // Form elements for adding a new dbxref
   //---------------------------------------------
   $form['addtl_dbxrefs']['dbxref_table']['new'] = array(
@@ -352,9 +403,9 @@ function chado_add_node_form_dbxrefs(&$form, &$form_state, $details) {
   $form['addtl_dbxrefs']['dbxref_table']['new']['dbxref_action'] = array(
     '#type' => 'submit',
     '#value' => t('Add'),
-    '#name' => "dbxref-add",
+    '#name' => "dbxrefs-add",
     '#ajax' => array(
-      'callback' => "chado_add_node_form_dbxrefs_ajax_update",
+      'callback' => "chado_add_node_form_subtable_ajax_update",
       'wrapper' => 'tripal-generic-edit-addtl_dbxrefs-table',
       'effect'   => 'fade',
       'method'   => 'replace',
@@ -367,8 +418,8 @@ function chado_add_node_form_dbxrefs(&$form, &$form_state, $details) {
     // array. In order to keep validate errors from the node form validate and Drupal
     // required errors for non-dbxref fields preventing the user from adding dbxrefs we
     // set the #limit_validation_errors below
-    '#validate' => array('chado_add_node_form_dbxrefs_add_button_validate'),
-    '#submit' => array('chado_add_node_form_dbxrefs_add_button_submit'),
+    '#validate' => array('chado_add_node_form_subtables_add_button_validate'),
+    '#submit' => array('chado_add_node_form_subtables_add_button_submit'),
     // Limit the validation of the form upon clicking this button to the dbxref_table tree
     // No other fields will be validated (ie: no fields from the main form or any other api
     // added form).
@@ -377,6 +428,10 @@ function chado_add_node_form_dbxrefs(&$form, &$form_state, $details) {
     )
   );
 
+  $form['addtl_dbxrefs']['admin_message'] = array(
+    '#type' => 'markup',
+    '#markup' => $tripal_message
+  );
 }
 
 /**
@@ -386,7 +441,7 @@ function chado_add_node_form_dbxrefs(&$form, &$form_state, $details) {
  * @ingroup tripal_core
  */
 function chado_add_node_form_dbxrefs_add_button_validate($form, &$form_state) {
-
+  
   // Ensure the db_id is supplied & Valid
   $db = chado_select_record(
     'db',
@@ -404,6 +459,7 @@ function chado_add_node_form_dbxrefs_add_button_validate($form, &$form_state) {
   if (empty($form_state['values']['dbxref_table']['new']['dbxref_accession'])) {
     form_set_error('dbxref_table][new][dbxref_accession','You must enter the accession before attempting to add a new database reference.');
   }
+
 }
 
 /**
@@ -414,7 +470,7 @@ function chado_add_node_form_dbxrefs_add_button_validate($form, &$form_state) {
  *
  * @ingroup tripal_core
  */
-function chado_add_node_form_dbxrefs_add_button_submit(&$form, &$form_state) {
+function chado_add_node_form_dbxrefs_add_button_submit($form, &$form_state) {
 
   // if the chado_additional_dbxrefs array is not set then this is the first time modifying the
   // dbxref table. this means we need to include all the dbxrefs from the db
@@ -426,12 +482,11 @@ function chado_add_node_form_dbxrefs_add_button_submit(&$form, &$form_state) {
   $dbxref = array(
     'db_id' => $form_state['values']['dbxref_table']['new']['db'],
     'db_name' => $form_state['values']['dbxref_table']['new']['db_name'],
-    'dbxref_id' => NULL,
+    'dbxref_id' => 'TEMP' . uniqid(),
     'version' => $form_state['values']['dbxref_table']['new']['dbxref_version'],
     'accession' => $form_state['values']['dbxref_table']['new']['dbxref_accession'],
   );
-  $version = (!empty($dbxref['version'])) ? $dbxref['version'] : 'NONE';
-  $key = $dbxref['db_id'] . '-' . $version;
+  $key = $dbxref['db_id'] . '-' . $dbxref['dbxref_id'];
   $form_state['chado_additional_dbxrefs'][$key] = (object) $dbxref;
 
 
@@ -440,20 +495,15 @@ function chado_add_node_form_dbxrefs_add_button_submit(&$form, &$form_state) {
   unset($form_state['input']['dbxref_table']['new']['db_name']);
   unset($form_state['input']['dbxref_table']['new']['dbxref_version']);
   unset($form_state['input']['dbxref_table']['new']['dbxref_accession']);
-
-  $form_state['rebuild'] = TRUE;
 }
 
 /**
- * There is no user input for the remove buttons so there is no need to validate
- * However, both a submit & validate need to be specified so this is just a placeholder
- *
  * Called by the many remove buttons in chado_add_node_form_dbxrefs
  *
  * @ingroup tripal_core
  */
 function chado_add_node_form_dbxrefs_remove_button_validate($form, $form_state) {
-  // No Validation needed for remove
+  // No validation needed.
 }
 
 /**
@@ -471,23 +521,12 @@ function chado_add_node_form_dbxrefs_remove_button_submit(&$form, &$form_state)
   }
 
   // remove the specified dbxref from the form dbxref table
-  if(preg_match('/dbxref_remove-([^-]+-[^-]+)/',$form_state['triggering_element']['#name'],$match)) {
+  if(preg_match('/dbxrefs_remove-([^-]+-[^-]+)/',$form_state['triggering_element']['#name'],$match)) {
     $key = $match[1];
     if (array_key_exists($key, $form_state['chado_additional_dbxrefs'])) {
       unset($form_state['chado_additional_dbxrefs'][$key]);
     }
   }
-
-  $form_state['rebuild'] = TRUE;
-}
-
-/**
- * Ajax function which returns the section of the form to be re-rendered
- *
- * @ingroup tripal_core
- */
-function chado_add_node_form_dbxrefs_ajax_update($form, $form_state) {
-  return $form['addtl_dbxrefs']['dbxref_table'];
 }
 
 /**
@@ -500,7 +539,7 @@ function chado_add_node_form_dbxrefs_ajax_update($form, $form_state) {
  *   '[db_id]-[version]' => array(
  *     'db_id' => [the db.db_id value]
  *     'db_name' => [the db.name value]
- *     'dbxref_id' => [the dbxref.dbxref_id value, or NULL if it doesn't yet exists],
+ *     'dbxref_id' => [the dbxref.dbxref_id value, or temporary value if it doesn't yet exists],
  *     'version' => [the dbxref.version value],
  *     'accession' => [the dbxref.accession value],
  *   ),
@@ -523,8 +562,7 @@ function chado_add_node_form_dbxrefs_create_dbxref_formstate_array($form, &$form
             'version' => $element['dbxref_version']['#markup'],
             'accession' => $element['dbxref_accession']['#markup'],
           );
-          $version = (!empty($dbxref['version'])) ? $dbxref['version'] : 'NONE';
-          $key = $dbxref['db_id'] . '-' . $version;
+          $key = $dbxref['db_id'] . '-' . $dbxref['dbxref_id'];
           $form_state['chado_additional_dbxrefs'][$key] = (object) $dbxref;
       }
     }
@@ -562,6 +600,7 @@ function theme_chado_add_node_form_dbxrefs_table($variables) {
         $row = array();
 
         $row['data'] = array();
+        $row['class'] = $element[$db_id][$version]['#attributes']['class'];
         foreach ($header as $fieldname => $title) {
           $row['data'][] = drupal_render($element[$db_id][$version][$fieldname]);
         }
@@ -570,7 +609,7 @@ function theme_chado_add_node_form_dbxrefs_table($variables) {
     }
   }
 
-  return theme('table', array(
+  return render($element['save_warning']) . theme('table', array(
     'header' => $header,
     'rows' => $rows
   ));
@@ -599,8 +638,8 @@ function chado_retrieve_node_form_dbxrefs($node) {
   if (isset($node->dbxref_table)) {
     foreach ($node->dbxref_table as $db_id => $elements) {
       if ($db_id != 'new') {
-        foreach ($elements as $version => $dbxref) {
-          $dbxref_id = (!empty($dbxref['dbxref_id'])) ? $dbxref['dbxref_id'] : 'NONE';
+        foreach ($elements as $dbxref_id => $dbxref) {
+          $version = (!empty($dbxref['version'])) ? $dbxref['version'] : 'NONE';
           $dbxrefs[$db_id][$version][$dbxref_id] = $dbxref['accession'];
         }
       }
@@ -647,7 +686,7 @@ function chado_update_node_form_dbxrefs($node, $details, $retrieved_dbxrefs = FA
       foreach ($versions as $version => $elements) {
         foreach ($elements as $dbxref_id => $accession) {
           // If there is no dbxref then we have to create that first
-          if ($dbxref_id == 'NONE') {
+          if (preg_match('/^TEMP/',$dbxref_id)) {
             $version = ($version == 'NONE') ? '' : $version;
             $success = tripal_insert_dbxref(array(
               'db_id' => $db_id,

+ 130 - 114
tripal_core/api/tripal_core.chado_nodes.properties.api.inc

@@ -172,8 +172,12 @@ function chado_add_node_form_properties(&$form, &$form_state, $details) {
   if (!array_key_exists('chado_id_field', $details)) {
     $chado_id_table = preg_replace('/prop$/', '', $details['property_table']);
     $chado_id_field = $chado_id_table . '_id';
+    $details['nodetype'] = $chado_id_table;
     $details['chado_id_field'] = $chado_id_field;
   }
+  else {
+    $details['nodetype'] = str_replace('_id', '', $details['chado_id_field']);
+  }
 
   // make sure the specified cv exists
   if (isset($details['cv_name'])) {
@@ -310,21 +314,37 @@ function chado_add_node_form_properties(&$form, &$form_state, $details) {
     );
   }
 
+  // Group all of the chado node api fieldsets into vertical tabs.
+  $form['chado_node_api'] = array(
+    '#type' => 'vertical_tabs',
+    '#attached' => array(
+      'css' => array(
+        'chado-node-api' => drupal_get_path('module', 'tripal_core') . '/theme/css/chado_node_api.css',
+      ),
+    ),
+  );
+
   // the fieldset of the property elements
+  $instructions = 'To add properties of the current %nodetype, select the type of 
+      information from the drop-down below and enter the information in the text box before 
+      clicking "Add". To remove incorrect information, click the "Remove" button. 
+      Note: you cannot edit previously added information but instead need to 
+      remove and re-add it.';
   $form['properties'] = array(
     '#type' => 'fieldset',
     '#title' => t($details['fieldset_title']),
-    '#description' => t('Add properties by selecting a type
-      from the dropdown, enter a value and click the "Add" button. To
-      remove a property, click the remove button.' . $details['additional_instructions']),
-    '#prefix' => "<div id='properties-fieldset'>",
-    '#suffix' => '</div>',
-    '#weight'      => 8
-  );
-
-  $form['properties']['admin_message'] = array(
-    '#type' => 'markup',
-    '#markup' => $tripal_message
+    '#description' => t('<p><strong>Additional information about a 
+      %nodetype.</strong></p><p>'. $instructions . $details['additional_instructions'] . '</p>', array('%nodetype' => $details['nodetype'])) ,
+    '#collapsible' => TRUE,
+    '#collapsed' => TRUE,
+    '#group' => 'chado_node_api',
+    '#weight'      => 8,
+    '#attributes' => array('class' => array('chado-node-api','properties')),
+    '#attached' => array(
+      'js' => array(
+        'chado-node-api-vertical-tabs' => drupal_get_path('module', 'tripal_core') . '/theme/js/chadoNodeApi_updateVerticalTabSummary.js',
+      ),
+    ),
   );
 
   // this form element is a tree, so that we don't puke all of the values into then node variable
@@ -337,6 +357,21 @@ function chado_add_node_form_properties(&$form, &$form_state, $details) {
     '#theme' => 'chado_node_properties_form_table'
   );
 
+  // We need to provide feedback to the user that changes made
+  // are not saved until the node is saved.
+  $form['properties']['property_table']['save_warning'] = array(
+    '#type' => 'markup',
+    '#prefix' => '<div id="property-save-warning" class="messages warning" style="display:none;">',
+    '#suffix' => '</div>',
+    '#markup' => '* The changes to these properties will not be saved until the 
+      "Save" button at the bottom of this form is clicked. <span class="specific-changes"></span>',
+    '#attached' => array(
+      'js' => array(
+        'chado-node-api-unsaved' => drupal_get_path('module', 'tripal_core') . '/theme/js/chadoNodeApi_unsavedNotify.js',
+      ),
+    ),
+  );
+
   // Add defaults into form_state to be used elsewhere
   $form['properties']['property_table']['details'] = array(
     '#type' => 'hidden',
@@ -358,7 +393,6 @@ function chado_add_node_form_properties(&$form, &$form_state, $details) {
     $existing_properties = $form_state['chado_properties'];
   }
   else {
-    $ranks = array(); // a temporary array used for calculating rank
 
     // build the SQL for extracting properties already assigned to this record
     $sql_args = array();
@@ -387,18 +421,6 @@ function chado_add_node_form_properties(&$form, &$form_state, $details) {
          $cv_where
        ORDER BY CVT.name, PP.rank", $sql_args)->fetchAll();
 
-    // iterate through the results and get the largest rank for each type
-    foreach ($existing_properties as $property) {
-      if (array_key_exists($property->type_id, $ranks)) {
-        if($ranks[$property->type_id] < $property->rank) {
-          $ranks[$property->type_id] = $property->rank;
-        }
-      }
-      else {
-        $ranks[$property->type_id] = $property->rank;
-      }
-    }
-
     // next add in any default properties
     if (array_key_exists('default_properties', $details)) {
 
@@ -410,16 +432,8 @@ function chado_add_node_form_properties(&$form, &$form_state, $details) {
         $new_prop->type_name   = $property['cvterm']->name;
         $new_prop->definition  = $property['cvterm']->definition;
         $new_prop->value       = $property['value'];
-        $new_prop->property_id = NULL;
-        // to set the rank for this property, we need to make sure we set
-        // it greater than any already existing rank
-        if (array_key_exists($property['cvterm']->cvterm_id, $ranks)) {
-          $ranks[$property['cvterm']->cvterm_id]++;
-        }
-        else {
-          $ranks[$property['cvterm']->cvterm_id] = 0;
-        }
-        $new_prop->rank = $ranks[$property['cvterm']->cvterm_id];
+        $new_prop->property_id = 'TEMP' . uniqid();
+        $new_prop->rank = 'TEMP' . uniqid();
         $existing_properties[] = $new_prop;
       }
     }
@@ -432,9 +446,9 @@ function chado_add_node_form_properties(&$form, &$form_state, $details) {
    *   '[type_id]-[rank]' => array(
    *     'type_id' => [the cvterm.cvterm_id value]
    *     'type_name' => [the cvterm.name value]
-   *     'property_id' => [the property.property_id value, or NULL if it doesn't yet exist],
+   *     'property_id' => [the property.property_id value, or temporary value if it doesn't yet exist],
    *     'value' => [the BASEprop.value value],
-   *     'rank' => [the BASEprop.rank value],
+   *     'rank' => [the BASEprop.rank value or NULL if not saved yet],
    *   ),
    * );
    *
@@ -442,7 +456,7 @@ function chado_add_node_form_properties(&$form, &$form_state, $details) {
    * Populated from the database:
    * $existing_property = array(
    *   0 => array(
-   *     'property_id' => [the property.property_id value, or NULL if it doesn't yet exist],
+   *     'property_id' => [the property.property_id value],
    *     'type_id' => [the cvterm.cvterm_id value]
    *     'type_name' => [the cvterm.name value]
    *     'value' => [the BASEprop.value value],
@@ -456,56 +470,76 @@ function chado_add_node_form_properties(&$form, &$form_state, $details) {
    * an element to the form for each one as long as it's also in the
    * $properties_options array.
    */
+  $num_properties = 0;
   foreach ($existing_properties as $property) {
     if (array_key_exists($property->type_id, $property_options)) {
+      $num_properties++;
 
       $form['properties']['property_table'][$property->type_id]['#type'] = 'markup';
       $form['properties']['property_table'][$property->type_id]['#value'] = '';
 
-      $form['properties']['property_table'][$property->type_id][$property->rank]['#type'] = 'markup';
-      $form['properties']['property_table'][$property->type_id][$property->rank]['#value'] = '';
+      $form['properties']['property_table'][$property->type_id][$property->property_id]['#type'] = 'markup';
+      $form['properties']['property_table'][$property->type_id][$property->property_id]['#value'] = '';
+      $form['properties']['property_table'][$property->type_id][$property->property_id]['#attributes'] = array(
+        'class' => array('property', 'saved')
+      );
+
+      // Determine whether this property is unsaved or not.
+      // We can tell this by looking at the property_id: if it's not
+      // saved yet we will have entered a TEMP###.
+      if (preg_match('/^TEMP/', $property->property_id)) {
+        $form['properties']['property_table'][$property->type_id][$property->property_id]['#attributes'] = array(
+          'class' => array('property', 'unsaved')
+        );
+      }
 
-      $form['properties']['property_table'][$property->type_id][$property->rank]['prop_type_id'] = array(
+      $form['properties']['property_table'][$property->type_id][$property->property_id]['prop_type_id'] = array(
         '#type' => 'hidden',
         '#value' => $property->type_id
       );
 
-      $form['properties']['property_table'][$property->type_id][$property->rank]['prop_value'] = array(
+      $form['properties']['property_table'][$property->type_id][$property->property_id]['prop_value'] = array(
         '#type' => 'hidden',
         '#value' => $property->value
       );
 
-      $form['properties']['property_table'][$property->type_id][$property->rank]['property_id'] = array(
+      $form['properties']['property_table'][$property->type_id][$property->property_id]['prop_rank'] = array(
+        '#type' => 'hidden',
+        '#value' => $property->rank
+      );
+
+      $form['properties']['property_table'][$property->type_id][$property->property_id]['property_id'] = array(
         '#type' => 'hidden',
         '#value' => $property->property_id
       );
 
-      $form['properties']['property_table'][$property->type_id][$property->rank]['type'] = array(
+      $form['properties']['property_table'][$property->type_id][$property->property_id]['type'] = array(
         '#type' => 'markup',
-        '#markup' => $property->type_name
+        '#markup' => $property->type_name,
+        '#prefix' => '<span class="row-unsaved-warning"></span>'
       );
       // If a definition is available we want to add that to the type column
       // to make it easier for users to determine what an added property means.
       if (isset($property->definition)) {
-        $form['properties']['property_table'][$property->type_id][$property->rank]['type']['#markup'] = $property->type_name . '<br><i>' . $property->definition . '</i>';
+        $form['properties']['property_table'][$property->type_id][$property->property_id]['type']['#markup'] = $property->type_name . '<br><i>' . $property->definition . '</i>';
       }
 
-      $form['properties']['property_table'][$property->type_id][$property->rank]['value'] = array(
+      $form['properties']['property_table'][$property->type_id][$property->property_id]['value'] = array(
         '#type' => 'markup',
         '#markup' => $property->value,
       );
 
-      $form['properties']['property_table'][$property->type_id][$property->rank]['rank'] = array(
+      $form['properties']['property_table'][$property->type_id][$property->property_id]['rank'] = array(
         '#type' => 'markup',
         '#markup' => $property->rank
       );
       // remove button
-      $form['properties']['property_table'][$property->type_id][$property->rank]['property_action'] = array(
+      $form['properties']['property_table'][$property->type_id][$property->property_id]['property_action'] = array(
         '#type' => 'submit',
         '#value' => t('Remove'),
-        '#name' => "property_remove-".$property->type_id.'-'.$property->rank,
+        '#name' => "properties_remove-".$property->type_id.'-'.$property->property_id,
         '#ajax' => array(
-          'callback' => "chado_add_node_form_properties_ajax_update",
+          'callback' => "chado_add_node_form_subtable_ajax_update",
           'wrapper' => 'tripal-generic-edit-properties-table',
           'effect'   => 'fade',
           'method'   => 'replace',
@@ -518,18 +552,25 @@ function chado_add_node_form_properties(&$form, &$form_state, $details) {
         // from the chado_properties array. In order to keep validate errors
         // from the node form validate and Drupal required errors for non-property fields
         // preventing the user from removing properties we set the #limit_validation_errors below
-        '#validate' => array('chado_add_node_form_properties_remove_button_validate'),
-        '#submit' => array('chado_add_node_form_properties_remove_button_submit'),
+        '#validate' => array('chado_add_node_form_subtables_remove_button_validate'),
+        '#submit' => array('chado_add_node_form_subtables_remove_button_submit'),
         // Limit the validation of the form upon clicking this button to the property_table tree
         // No other fields will be validated (ie: no fields from the main form or any other api
         // added form).
         '#limit_validation_errors' => array(
           array('property_table')  // Validate all fields within $form_state['values']['property_table']
-        )
+        ),
       );
     }
   }
 
+  // Quickly add a hidden field stating how many properties are currently added.
+  $form['properties']['num_properties'] = array(
+    '#type' => 'hidden',
+    '#value' => $num_properties,
+    '#attributes' => array('class' => 'num-properties')
+  );
+
   // Form elements for adding a new property
   //---------------------------------------------
   $form['properties']['property_table']['new'] = array(
@@ -570,9 +611,9 @@ function chado_add_node_form_properties(&$form, &$form_state, $details) {
   $form['properties']['property_table']['new']['property_action'] = array(
     '#type' => 'submit',
     '#value' => t('Add'),
-    '#name' => "property-add",
+    '#name' => "properties-add",
     '#ajax' => array(
-      'callback' => "chado_add_node_form_properties_ajax_update",
+      'callback' => "chado_add_node_form_subtable_ajax_update",
       'wrapper' => 'tripal-generic-edit-properties-table',
       'effect'   => 'fade',
       'method'   => 'replace',
@@ -585,8 +626,8 @@ function chado_add_node_form_properties(&$form, &$form_state, $details) {
     // array. In order to keep validate errors from the node form validate and Drupal
     // required errors for non-property fields preventing the user from adding properties we
     // set the #limit_validation_errors below
-    '#validate' => array('chado_update_node_form_properties_add_button_validate'),
-    '#submit' => array('chado_add_node_form_properties_add_button_submit'),
+    '#validate' => array('chado_add_node_form_subtables_add_button_validate'),
+    '#submit' => array('chado_add_node_form_subtables_add_button_submit'),
     // Limit the validation of the form upon clicking this button to the property_table tree
     // No other fields will be validated (ie: no fields from the main form or any other api
     // added form).
@@ -594,6 +635,11 @@ function chado_add_node_form_properties(&$form, &$form_state, $details) {
       array('property_table')  // Validate all fields within $form_state['values']['property_table']
     )
   );
+
+  $form['properties']['admin_message'] = array(
+    '#type' => 'markup',
+    '#markup' => $tripal_message
+  );
 }
 
 /**
@@ -602,8 +648,8 @@ function chado_add_node_form_properties(&$form, &$form_state, $details) {
  *
  * @ingroup tripal_core
  */
-function chado_update_node_form_properties_add_button_validate($form, &$form_state) {
-
+function chado_add_node_form_properties_add_button_validate($form, &$form_state) {
+  
   // Ensure the type_id is supplied & Valid
   $cvterm = chado_select_record(
     'cvterm',
@@ -632,7 +678,7 @@ function chado_update_node_form_properties_add_button_validate($form, &$form_sta
  *
  * @ingroup tripal_core
  */
-function chado_add_node_form_properties_add_button_submit(&$form, &$form_state) {
+function chado_add_node_form_properties_add_button_submit($form, &$form_state) {
 
   $details = unserialize($form_state['values']['property_table']['details']);
 
@@ -647,40 +693,13 @@ function chado_add_node_form_properties_add_button_submit(&$form, &$form_state)
     'type_id' => $form_state['values']['property_table']['new']['type'],
     'type_name' => $form_state['values']['property_table']['new']['type_name'],
     'definition' => $form_state['values']['property_table']['new']['definition'],
-    'property_id' => NULL,
+    'property_id' => 'TEMP' . uniqid(),
     'value' => $form_state['values']['property_table']['new']['value'],
-    'rank' => '0',
+    'rank' => 'TEMP' . uniqid(),
   );
 
-  // Determine the rank for the new property based on the the data already
-  // stored in the properties table.
-  $rank = chado_get_table_max_rank(
-    $details['property_table'],
-    array(
-      $details['chado_id_field'] => $details['chado_id'],
-      'type_id' => $property['type_id']
-    )
-  );
-  $property['rank'] = strval($rank + 1);
-  $key = $property['type_id'] . '-' . $property['rank'];
-
-  // Now check to make sure a property doesn't already exist with that rank
-  // (which happens when 2+ properties of the same type are added within the
-  // same save).
-  if (isset($form_state['chado_properties'][$key])) {
-    // Then keep iterating the rank until you find there is no property in
-    // the properties list with the same type/rank combination.
-    do {
-      $property['rank']++;
-      $key = $property['type_id'] . '-' . $property['rank'];
-    } while (isset($form_state['chado_properties'][$key]));
-
-    // And then set the property to that free space.
-    $form_state['chado_properties'][$key] = (object) $property;
-  }
-  else {
-    $form_state['chado_properties'][$key] = (object) $property;
-  }
+  $key = $property['type_id'] . '-' . $property['property_id'];
+  $form_state['chado_properties'][$key] = (object) $property;
 
   // we don't want the new element to pick up the values from the previous element so wipe them out
   unset($form_state['input']['property_table']['new']['type']);
@@ -688,19 +707,15 @@ function chado_add_node_form_properties_add_button_submit(&$form, &$form_state)
   unset($form_state['input']['property_table']['new']['definition']);
   unset($form_state['input']['property_table']['new']['value']);
 
-  $form_state['rebuild'] = TRUE;
 }
 
 /**
- * There is no user input for the remove buttons so there is no need to validate
- * However, both a submit & validate need to be specified so this is just a placeholder
- *
  * Called by the many remove buttons in chado_add_node_form_properties
  *
  * @ingroup tripal_core
  */
-function chado_add_node_form_properties_remove_button_validate($form, $form_state) {
-  // No Validation needed for remove
+function chado_add_node_form_properties_remove_button_validate($form, &$form_state) {
+  // No validation needed.
 }
 
 /**
@@ -718,27 +733,17 @@ function chado_add_node_form_properties_remove_button_submit(&$form, &$form_stat
   }
 
   // remove the specified property from the form property table
-  if(preg_match('/property_remove-([^-]+-[^-]+)/',$form_state['triggering_element']['#name'],$match)) {
+  if(preg_match('/properties_remove-([^-]+-[^-]+)/',$form_state['triggering_element']['#name'],$match)) {
     $key = $match[1];
     if (array_key_exists($key, $form_state['chado_properties'])) {
       unset($form_state['chado_properties'][$key]);
     }
   }
-
-  $form_state['rebuild'] = TRUE;
 }
 
 function chado_add_node_form_properties_ajax_desc($form, $form_state) {
   return $form['properties']['property_table']['new']['type'];
 }
-/**
- * Ajax function which returns the section of the form to be re-rendered
- *
- * @ingroup tripal_core
- */
-function chado_add_node_form_properties_ajax_update($form, $form_state) {
-  return $form['properties']['property_table'];
-}
 
 /**
  * Creates an array in form_state containing the existing properties. This array is
@@ -764,8 +769,8 @@ function chado_add_node_form_properties_create_property_formstate_array($form, &
 
   foreach (element_children($form['properties']['property_table']) as $type_id) {
     if ($type_id != 'new') {
-      foreach (element_children($form['properties']['property_table'][$type_id]) as $rank) {
-          $element = $form['properties']['property_table'][$type_id][$rank];
+      foreach (element_children($form['properties']['property_table'][$type_id]) as $property_id) {
+          $element = $form['properties']['property_table'][$type_id][$property_id];
           $property = array(
             'type_id' => $element['prop_type_id']['#value'],
             'type_name' => $element['type']['#markup'],
@@ -773,7 +778,7 @@ function chado_add_node_form_properties_create_property_formstate_array($form, &
             'value' => $element['value']['#markup'],
             'rank' => $element['rank']['#markup']
           );
-          $key = $property['type_id'] . '-' . $property['rank'];
+          $key = $property['type_id'] . '-' . $property['property_id'];
           $form_state['chado_properties'][$key] = (object) $property;
       }
     }
@@ -810,6 +815,7 @@ function theme_chado_add_node_form_properties($variables) {
         $row = array();
 
         $row['data'] = array();
+        $row['class'] = $element[$type_id][$version]['#attributes']['class'];
         foreach ($header as $fieldname => $title) {
           $row['data'][] = drupal_render($element[$type_id][$version][$fieldname]);
         }
@@ -818,7 +824,7 @@ function theme_chado_add_node_form_properties($variables) {
     }
   }
 
-  return theme('table', array(
+  return render($element['save_warning']) . theme('table', array(
     'header' => $header,
     'rows' => $rows
   ));
@@ -846,8 +852,8 @@ function chado_retrieve_node_form_properties($node) {
   if (isset($node->property_table)) {
     foreach ($node->property_table as $type_id => $elements) {
       if ($type_id != 'new' AND $type_id != 'details') {
-        foreach ($elements as $rank => $element) {
-          $properties[$type_id][$rank] = $element['prop_value'];
+        foreach ($elements as $property_id => $element) {
+          $properties[$type_id][$element['prop_rank']] = $element['prop_value'];
         }
       }
     }
@@ -891,6 +897,16 @@ function chado_update_node_form_properties($node, $details, $retrieved_propertie
     foreach ($properties as $type_id => $ranks) {
       foreach ($ranks as $rank => $value) {
 
+        if (preg_match('/^TEMP/', $rank)) {
+          $rank = chado_get_table_max_rank(
+            $details['property_table'],
+            array(
+              $details['foreignkey_name'] => $details['foreignkey_value'],
+              'type_id' => $type_id
+            )
+          );
+          $rank = strval($rank + 1);
+        }
         $success = chado_insert_record(
           $details['property_table'],
           array(

+ 119 - 75
tripal_core/api/tripal_core.chado_nodes.relationships.api.inc

@@ -275,26 +275,43 @@ function chado_add_node_form_relationships(&$form, &$form_state, $details) {
     );
   }
 
+  // Group all of the chado node api fieldsets into vertical tabs.
+  $form['chado_node_api'] = array(
+    '#type' => 'vertical_tabs',
+    '#attached' => array(
+      'css' => array(
+        'chado-node-api' => drupal_get_path('module', 'tripal_core') . '/theme/css/chado_node_api.css',
+      ),
+    ),
+  );
+
   $form['relationships'] = array(
     '#type' => 'fieldset',
     '#title' => t($details['fieldset_title']),
-    '#prefix' => "<div id='relationships-fieldset'>",
-    '#suffix' => '</div>',
-    '#weight'      => 10
+    '#collapsible' => TRUE,
+    '#collapsed' => TRUE,
+    '#group' => 'chado_node_api',
+    '#weight'      => 10,
+    '#attributes' => array('class' => array('chado-node-api','relationships')),
+    '#attached' => array(
+      'js' => array(
+        'chado-node-api-vertical-tabs' => drupal_get_path('module', 'tripal_core') . '/theme/js/chadoNodeApi_updateVerticalTabSummary.js',
+      ),
+    ),
   );
 
+  $instructions = 'Relationships should be read like a sentence ([subject] [type] 
+    [object]) in order to determine their direction and thus their meaning. When 
+    adding a relationship, it is easiest to first select the type of relationship you would 
+    like to enter and then select whether the current %nodetype is the subject
+    or object (based on which "sentence" makes sense). Finally enter the other 
+    %nodetype in the remaining text box (making sure to select from the 
+    autocomplete drop-down) before clicking "Add". To remove incorrect relationships, click the 
+    "Remove" button. Note: you cannot edit previously added relationships 
+    but instead need to remove and re-add them.';
   $form['relationships']['descrip'] = array(
     '#type' => 'item',
-    '#markup' => t('You may add relationships between this %nodetype and other
-      %nodetype_plural by entering the details below.  You may add
-      as many relationships as desired by clicking the add button on the right.  To
-      remove a relationship, click the remove button. ' . $details['additional_instructions'],
-      array('%nodetype' => $details['nodetype'], '%nodetype_plural' => $details['nodetype_plural'])),
-  );
-
-  $form['relationships']['admin_message'] = array(
-    '#type' => 'markup',
-    '#markup' => $tripal_message
+    '#markup' => t('<p><strong>Relate the current %nodetype with others that already exist.</strong></p><p>'. $instructions . $details['additional_instructions'] . '</p>', array('%nodetype' => $details['nodetype'])),
   );
 
   // this form element is a tree, so that we don't puke all of the values into then node variable
@@ -313,6 +330,20 @@ function chado_add_node_form_relationships(&$form, &$form_state, $details) {
     '#value' => serialize($details)
   );
 
+  // We need to provide feedback to the user that changes made
+  // are not saved until the node is saved.
+  $form['relationships']['relationship_table']['save_warning'] = array(
+    '#type' => 'markup',
+    '#prefix' => '<div id="relationship-save-warning" class="messages warning" style="display:none;">',
+    '#suffix' => '</div>',
+    '#markup' => '* The changes to these relationships will not be saved until the "Save" button at the bottom of this form is clicked. <span class="specific-changes"></span>',
+    '#attached' => array(
+      'js' => array(
+        'chado-node-api-unsaved' => drupal_get_path('module', 'tripal_core') . '/theme/js/chadoNodeApi_unsavedNotify.js',
+      ),
+    ),
+  );
+
   // Add relationships already attached to the node
   //---------------------------------------------
   /* Relationships can come to us in two ways:
@@ -332,6 +363,7 @@ function chado_add_node_form_relationships(&$form, &$form_state, $details) {
     $existing_rels = chado_query(
       "SELECT
           rel.*,
+          rel.".$details['relationship_table']."_id as relationship_id,
           rel.".$details['subject_field_name']." as subject_id,
           rel.".$details['object_field_name']." as object_id,
           base1.".$details['base_name_field']." as object_name,
@@ -351,14 +383,15 @@ function chado_add_node_form_relationships(&$form, &$form_state, $details) {
    *
    * From the chado_relationships array:
    * $form_state['chado_relationships'] = array(
-   *   '[type_id]-[rank]' => array(
+   *   '[type_id]-[_relationship_id]' => array(
+   *     'relationship_id' => [the _relationship._relationship_id value OR a temporary value if not yet saved to the database],
    *     'object_id' => [the _relationship.object_id value],
    *     'object_name' => [the base_table.uniquename value linked on base_foreign_key=object_id],
    *     'subject_id' => [the _relationship.subject_id value],
    *     'subject_name' => [the base_table.uniquename value linked on base_foreign_key=subject_id],
    *     'type_id' => [the _relationship.type_id value],
    *     'type_name' => [the cvterm.name value linked on type_id],
-   *     'rank' => [the _relationship.rank value],
+   *     'rank' => [the _relationship.rank value OR NULL if not yet saved to the database],
    *   ),
    * );
    *
@@ -366,7 +399,7 @@ function chado_add_node_form_relationships(&$form, &$form_state, $details) {
    * Populated from the database:
    * $existing_rels = array(
    *   0 => array(
-   *     'relationship_id' => [the _relationship.relationship_id value],
+   *     'relationship_id' => [the _relationship._relationship_id value],
    *     'object_id' => [the _relationship.object_id value],
    *     'object_name' => [the base_table.uniquename value linked on base_foreign_key=object_id],
    *     'subject_id' => [the _relationship.subject_id value],
@@ -382,62 +415,76 @@ function chado_add_node_form_relationships(&$form, &$form_state, $details) {
    * Loop on the array elements of the $existing_rels array and add
    * an element to the form for each one.
    */
+  $num_relationships = 0;
   foreach ($existing_rels as $relationship) {
     if (array_key_exists($relationship->type_id, $type_options)) {
-
-      // We're using a unique id as a rank placeholder to ensure all relationships are shown
-      // We can't use the actual rank b/c there can be two relationships with the same type
-      // and rank as long as the subject_id and object_id are switched. For example, you can
-      // have Fred is_paternal_parent_of Max and Max is_paternal_parent_of Lui (both rank=0)
-      $rank = uniqid();
+      $num_relationships++;
 
       $form['relationships']['relationship_table'][$relationship->type_id]['#type'] = 'markup';
       $form['relationships']['relationship_table'][$relationship->type_id]['#type'] = '';
 
-      $form['relationships']['relationship_table'][$relationship->type_id][$rank]['#type'] = 'markup';
-      $form['relationships']['relationship_table'][$relationship->type_id][$rank]['#value'] = '';
+      $form['relationships']['relationship_table'][$relationship->type_id][$relationship->relationship_id]['#type'] = 'markup';
+      $form['relationships']['relationship_table'][$relationship->type_id][$relationship->relationship_id]['#value'] = '';
+      $form['relationships']['relationship_table'][$relationship->type_id][$relationship->relationship_id]['#attributes'] = array(
+        'class' => array('relationship', 'saved')
+      );
 
-      $form['relationships']['relationship_table'][$relationship->type_id][$rank]['object_id'] = array(
+      // Determine whether this relationship is unsaved or not.
+      // We can tell this by looking at the relationship_id: if it's not
+      // saved yet we will have entered a TEMP###.
+      if (preg_match('/^TEMP/', $relationship->relationship_id)) {
+        $form['relationships']['relationship_table'][$relationship->type_id][$relationship->relationship_id]['#attributes'] = array(
+          'class' => array('relationship', 'unsaved')
+        );
+      }
+
+      $form['relationships']['relationship_table'][$relationship->type_id][$relationship->relationship_id]['relationship_id'] = array(
+        '#type' => 'markup',
+        '#markup' => $relationship->relationship_id
+      );
+
+      $form['relationships']['relationship_table'][$relationship->type_id][$relationship->relationship_id]['object_id'] = array(
         '#type' => 'hidden',
         '#value' => $relationship->object_id
       );
 
-      $form['relationships']['relationship_table'][$relationship->type_id][$rank]['subject_id'] = array(
+      $form['relationships']['relationship_table'][$relationship->type_id][$relationship->relationship_id]['subject_id'] = array(
         '#type' => 'hidden',
         '#value' => $relationship->subject_id
       );
 
-      $form['relationships']['relationship_table'][$relationship->type_id][$rank]['type_id'] = array(
+      $form['relationships']['relationship_table'][$relationship->type_id][$relationship->relationship_id]['type_id'] = array(
         '#type' => 'hidden',
         '#value' => $relationship->type_id
       );
 
-      $form['relationships']['relationship_table'][$relationship->type_id][$rank]['object_name'] = array(
+      $form['relationships']['relationship_table'][$relationship->type_id][$relationship->relationship_id]['object_name'] = array(
         '#type' => 'markup',
         '#markup' => $relationship->object_name
       );
 
-      $form['relationships']['relationship_table'][$relationship->type_id][$rank]['type_name'] = array(
+      $form['relationships']['relationship_table'][$relationship->type_id][$relationship->relationship_id]['type_name'] = array(
         '#type' => 'markup',
         '#markup' => $relationship->type_name
       );
 
-      $form['relationships']['relationship_table'][$relationship->type_id][$rank]['subject_name'] = array(
+      $form['relationships']['relationship_table'][$relationship->type_id][$relationship->relationship_id]['subject_name'] = array(
         '#type' => 'markup',
-        '#markup' => $relationship->subject_name
+        '#markup' => $relationship->subject_name,
+        '#prefix' => '<span class="row-unsaved-warning"></span>'
       );
 
-      $form['relationships']['relationship_table'][$relationship->type_id][$rank]['rank'] = array(
+      $form['relationships']['relationship_table'][$relationship->type_id][$relationship->relationship_id]['rank'] = array(
         '#type' => 'markup',
-        '#markup' => $rank
+        '#markup' => (isset($relationship->rank)) ? $relationship->rank : NULL
       );
 
-      $form['relationships']['relationship_table'][$relationship->type_id][$rank]['rel_action'] = array(
+      $form['relationships']['relationship_table'][$relationship->type_id][$relationship->relationship_id]['rel_action'] = array(
         '#type' => 'submit',
         '#value' => t('Remove'),
-        '#name' => "rel_remove-".$relationship->type_id.'-'.$rank,
+        '#name' => "relationships_remove-".$relationship->type_id.'-'.$relationship->relationship_id,
         '#ajax' => array(
-          'callback' => 'chado_add_node_form_relationships_ajax_update',
+          'callback' => 'chado_add_node_form_subtable_ajax_update',
           'wrapper' => 'tripal-generic-edit-relationships-table',
           'effect'   => 'fade',
           'method'   => 'replace',
@@ -450,8 +497,8 @@ function chado_add_node_form_relationships(&$form, &$form_state, $details) {
         // from the chado_relationships array. In order to keep validate errors
         // from the node form validate and Drupal required errors for non-relationship fields
         // preventing the user from removing relationships we set the #limit_validation_errors below
-        '#validate' => array('chado_add_node_form_relationships_form_remove_button_validate'),
-        '#submit' => array('chado_add_node_form_relationships_remove_button_submit'),
+        '#validate' => array('chado_add_node_form_subtables_remove_button_validate'),
+        '#submit' => array('chado_add_node_form_subtables_remove_button_submit'),
         // Limit the validation of the form upon clicking this button to the relationship_table tree
         // No other fields will be validated (ie: no fields from the main form or any other api
         // added form).
@@ -462,6 +509,15 @@ function chado_add_node_form_relationships(&$form, &$form_state, $details) {
     }
   }
 
+  // Quickly add a hidden field stating how many relationships are currently added.
+  $form['relationships']['num_relationships'] = array(
+    '#type' => 'hidden',
+    '#value' => $num_relationships,
+    '#attributes' => array('class' => 'num-relationships')
+  );
+
+  // Form elements for adding a new relationship
+  //---------------------------------------------
   $form['relationships']['relationship_table']['new']['object_name'] = array(
     '#type' => 'textfield',
     '#autocomplete_path' => 'tripal_ajax/relationship_nodeform/' . $details['base_table'] . '/' . $details['base_name_field'].'/name_to_id'
@@ -495,9 +551,9 @@ function chado_add_node_form_relationships(&$form, &$form_state, $details) {
   $form['relationships']['relationship_table']['new']['rel_action'] = array(
     '#type' => 'submit',
     '#value' => t('Add'),
-    '#name' => 'rel-add',
+    '#name' => 'relationships-add',
     '#ajax' => array(
-      'callback' => 'chado_add_node_form_relationships_ajax_update',
+      'callback' => 'chado_add_node_form_subtable_ajax_update',
       'wrapper' => 'tripal-generic-edit-relationships-table',
       'effect'   => 'fade',
       'method'   => 'replace',
@@ -509,8 +565,8 @@ function chado_add_node_form_relationships(&$form, &$form_state, $details) {
     // array. In order to keep validate errors from the node form validate and Drupal
     // required errors for non-relationship fields preventing the user from adding relationships we
     // set the #limit_validation_errors below
-    '#validate' => array('chado_add_node_form_relationships_add_button_validate'),
-    '#submit' => array('chado_add_node_form_relationships_add_button_submit'),
+    '#validate' => array('chado_add_node_form_subtables_add_button_validate'),
+    '#submit' => array('chado_add_node_form_subtables_add_button_submit'),
     // Limit the validation of the form upon clicking this button to the relationship_table tree
     // No other fields will be validated (ie: no fields from the main form or any other api
     // added form).
@@ -519,6 +575,10 @@ function chado_add_node_form_relationships(&$form, &$form_state, $details) {
     )
   );
 
+  $form['relationships']['admin_message'] = array(
+    '#type' => 'markup',
+    '#markup' => $tripal_message
+  );
 }
 
 /**
@@ -530,7 +590,7 @@ function chado_add_node_form_relationships(&$form, &$form_state, $details) {
 function chado_add_node_form_relationships_add_button_validate($form, &$form_state) {
 
   $details = unserialize($form_state['values']['relationship_table']['details']);
-
+  
   // First deal with autocomplete fields.
   // Extract the base_id assuming '(###) NAME FIELD'.
   if (!empty($form_state['values']['relationship_table']['new']['subject_name'])) {
@@ -655,7 +715,7 @@ function chado_add_node_form_relationships_add_button_validate($form, &$form_sta
  *
  * @ingroup tripal_core
  */
-function chado_add_node_form_relationships_add_button_submit(&$form, &$form_state) {
+function chado_add_node_form_relationships_add_button_submit($form, &$form_state) {
 
 
   $details = unserialize($form_state['values']['relationship_table']['details']);
@@ -677,7 +737,8 @@ function chado_add_node_form_relationships_add_button_submit(&$form, &$form_stat
       'object_name' => $form_state['values']['relationship_table']['new']['object_name'],
       'subject_id' => $form_state['node']->{$details['base_table']}->{$details['base_foreign_key']},
       'subject_name' => $name,
-      'rank' => uniqid(),
+      'rank' => NULL,
+      'relationship_id' => 'TEMP' . uniqid()
     );
     // we don't want the new element to pick up the values from the previous element so wipe them out
     unset($form_state['input']['relationship_table']['new']['object_id']);
@@ -691,35 +752,29 @@ function chado_add_node_form_relationships_add_button_submit(&$form, &$form_stat
       'object_name' => $name,
       'subject_id' => $form_state['values']['relationship_table']['new']['subject_id'],
       'subject_name' => $form_state['values']['relationship_table']['new']['subject_name'],
-      'rank' => uniqid(),
+      'rank' => NULL,
+      'relationship_id' => 'TEMP' . uniqid(),
     );
     // we don't want the new element to pick up the values from the previous element so wipe them out
     unset($form_state['input']['relationship_table']['new']['subject_id']);
     unset($form_state['input']['relationship_table']['new']['subject_name']);
   }
 
-  $key = $relationship['type_id'] . '-' . $relationship['rank'];
+  $key = $relationship['type_id'] . '-' . $relationship['relationship_id'];
   $form_state['chado_relationships'][$key] = (object) $relationship;
 
   // we don't want the new element to pick up the values from the previous element so wipe them out
   unset($form_state['input']['relationship_table']['new']['type_id']);
   unset($form_state['input']['relationship_table']['new']['type_name']);
-
-  // This is needed to ensure the form builder function is called to
-  // rebuild the form on ajax requests.
-  $form_state['rebuild'] = TRUE;
 }
 
 /**
- * There is no user input for the remove buttons so there is no need to validate
- * However, both a submit & validate need to be specified so this is just a placeholder
- *
  * Called by the many remove buttons in chado_add_node_form_relationships
  *
  * @ingroup tripal_core
  */
-function chado_add_node_form_relationships_form_remove_button_validate($form, $form_state) {
-  // No Validation needed for remove
+function chado_add_node_form_relationships_remove_button_validate($form, &$form_state) {
+  // No validation needed.
 }
 
 /**
@@ -735,25 +790,12 @@ function chado_add_node_form_relationships_remove_button_submit(&$form, &$form_s
   chado_add_node_form_relationships_create_relationship_formstate_array($form, $form_state);
 
   // remove the specified relationship from the form relationship table
-  if(preg_match('/rel_remove-([^-]+-[^-]+)/',$form_state['triggering_element']['#name'],$match)) {
+  if(preg_match('/relationships_remove-([^-]+-[^-]+)/',$form_state['triggering_element']['#name'],$match)) {
     $key = $match[1];
     if (array_key_exists($key, $form_state['chado_relationships'])) {
       unset($form_state['chado_relationships'][$key]);
     }
   }
-
-  // This is needed to ensure the form builder function is called to
-  // rebuild the form on ajax requests.
-  $form_state['rebuild'] = TRUE;
-}
-
-/**
- * Ajax function which returns the section of the form to be re-rendered
- *
- * @ingroup tripal_core
- */
-function chado_add_node_form_relationships_ajax_update($form, $form_state) {
-  return $form['relationships']['relationship_table'];
 }
 
 /**
@@ -780,8 +822,8 @@ function chado_add_node_form_relationships_create_relationship_formstate_array($
 
   foreach (element_children($form['relationships']['relationship_table']) as $type_id) {
     if ($type_id != 'new') {
-      foreach (element_children($form['relationships']['relationship_table'][$type_id]) as $rank) {
-          $element = $form['relationships']['relationship_table'][$type_id][$rank];
+      foreach (element_children($form['relationships']['relationship_table'][$type_id]) as $relationship_id) {
+          $element = $form['relationships']['relationship_table'][$type_id][$relationship_id];
           $rel = array(
             'type_id' => $element['type_id']['#value'],
             'object_id' => $element['object_id']['#value'],
@@ -789,9 +831,10 @@ function chado_add_node_form_relationships_create_relationship_formstate_array($
             'type_name' => $element['type_name']['#markup'],
             'object_name' => $element['object_name']['#markup'],
             'subject_name' => $element['subject_name']['#markup'],
-            'rank' => $element['rank']['#markup']
+            'rank' => $element['rank']['#markup'],
+            'relationship_id' => $relationship_id,
           );
-          $key = $rel['type_id'] . '-' . $rel['rank'];
+          $key = $rel['type_id'] . '-' . $rel['relationship_id'];
           $form_state['chado_relationships'][$key] = (object) $rel;
       }
     }
@@ -839,6 +882,7 @@ function theme_chado_add_node_form_relationships_table($variables) {
         $row = array();
 
         $row['data'] = array();
+        $row['class'] = $element[$type_id][$rank]['#attributes']['class'];
         foreach ($header as $fieldname => $title) {
           $row['data'][] = drupal_render($element[$type_id][$rank][$fieldname]);
         }
@@ -847,7 +891,7 @@ function theme_chado_add_node_form_relationships_table($variables) {
     }
   }
 
-  return theme('table', array(
+  return render($element['save_warning']) . theme('table', array(
     'header' => $header,
     'rows' => $rows,
     'sticky' => FALSE

+ 15 - 0
tripal_core/theme/css/chado_node_api.css

@@ -0,0 +1,15 @@
+
+.chado-node-api.new {
+  color: #3A7127;
+  font-style: italic;
+  font-weight: bold;
+}
+.chado-node-api.removed {
+  color: #680000;
+  font-style: italic;
+  font-weight: bold;
+}
+tr.unsaved td {
+  background-color: #fffce5;
+  color: #840;
+}

+ 82 - 0
tripal_core/theme/js/chadoNodeApi_unsavedNotify.js

@@ -0,0 +1,82 @@
+
+(function ($) {
+
+  Drupal.behaviors.chadoNodeApiChangeNotify = {
+    attach: function (context) {
+
+    ChadoNodeApi_notifyChanges({
+      machineName: {
+        plural: 'properties',
+        singular:'property'
+      },
+      readableName: {
+        plural: 'properties',
+        singular:'property'
+      }
+    });
+
+    ChadoNodeApi_notifyChanges({
+        machineName: {
+          plural: 'dbxrefs',
+          singular:'dbxref'
+        },
+        readableName: {
+          plural: 'references',
+          singular:'reference'
+        }
+      });
+
+    ChadoNodeApi_notifyChanges({
+        machineName: {
+          plural: 'relationships',
+          singular:'relationship'
+        },
+        readableName: {
+          plural: 'relationships',
+          singular:'relationship'
+        }
+      });
+    
+    function ChadoNodeApi_notifyChanges(api) {
+
+      var numCurrent = $('tr.' + api.machineName.singular).length;
+      var numOriginal = $('input.num-' + api.machineName.plural, context).val();
+      var numSaved = $('tr.saved.' + api.machineName.singular).length;
+      var numUnsaved = $('tr.unsaved.' + api.machineName.singular).length;
+      var numRemoved = numOriginal - numSaved;
+
+      // If changes have been made then notify the user.
+      if (numUnsaved > 0 || numRemoved > 0) {
+    	// Make the warning visible.
+    	$('#' + api.machineName.singular + '-save-warning').css("display","inherit");
+
+    	// Determine singular versus plural.
+    	var unsavedReadable = api.readableName.plural;
+        if (numUnsaved == 1) {
+          unsavedReadable = api.readableName.singular;
+        }
+        var removedReadable = api.readableName.plural;
+        if (numRemoved == 1) {
+          removedReadable = api.readableName.singular;
+        }
+        
+    	// Specify the changes made in the warning.
+    	var note = '';
+    	if (numUnsaved > 0 && numRemoved > 0) {
+    		note = 'NOTE: Changes include the addition of ' + numUnsaved + ' ' + unsavedReadable + ' and the removal of ' + numRemoved + ' saved ' + removedReadable + '.';
+    	}
+    	else if (numUnsaved > 0) {
+    		note = 'NOTE: Changes include the addition of ' + numUnsaved + ' ' + unsavedReadable + '.';
+    	}
+    	else if (numRemoved > 0) {
+    		note = 'NOTE: Changes include the removal of ' + numRemoved + ' saved ' + removedReadable + '.';
+    	}
+    	$('#' + api.machineName.singular + '-save-warning span.specific-changes').html(note);
+    	
+    	// Add a * to any new records to make the warning more accessible.
+    	$('tr.unsaved.' + api.machineName.singular + ' span.row-unsaved-warning').html('*');
+
+      }
+    }
+  }};
+})(jQuery);

+ 90 - 0
tripal_core/theme/js/chadoNodeApi_updateVerticalTabSummary.js

@@ -0,0 +1,90 @@
+
+(function ($) {
+
+  Drupal.behaviors.chadoNodeApiFieldsetSummaries = {
+    attach: function (context) {
+    	
+      // Properties Tab
+      $('fieldset.chado-node-api.properties', context).drupalSetSummary(function (context) {
+        return ChadoNodeApi_getSummary({
+          machineName: {
+            plural: 'properties',
+            singular:'property'
+          },
+          readableName: {
+            plural: 'properties',
+            singular:'property'
+          }
+        });
+      });
+
+      // External References Tab
+      $('fieldset.chado-node-api.dbxrefs', context).drupalSetSummary(function (context) {
+        return ChadoNodeApi_getSummary({
+          machineName: {
+            plural: 'dbxrefs',
+            singular:'dbxref'
+          },
+          readableName: {
+            plural: 'references',
+            singular:'reference'
+          }
+        });
+      });
+ 
+      // Relationships Tab
+      $('fieldset.chado-node-api.relationships', context).drupalSetSummary(function (context) {
+        return ChadoNodeApi_getSummary({
+          machineName: {
+            plural: 'relationships',
+            singular:'relationship'
+          },
+          readableName: {
+            plural: 'relationships',
+            singular:'relationship'
+          }
+        });
+      });
+
+      function ChadoNodeApi_getSummary(api) {
+
+        var numCurrent = $('tr.' + api.machineName.singular).length;
+        var numOriginal = $('input.num-' + api.machineName.plural, context).val();
+        var numSaved = $('tr.saved.' + api.machineName.singular).length;
+        var numUnsaved = $('tr.unsaved.' + api.machineName.singular).length;
+        var numRemoved = numOriginal - numSaved;
+
+
+        // If there are no rows then tell the user that.
+        if (numCurrent == 0) {
+          if (numRemoved == 0) {
+            return Drupal.t('No ' + api.readableName.plural);
+          }
+          else {
+            return Drupal.t('No ' + api.readableName.plural + ' (<span class="chado-node-api removed">' + numRemoved + ' removed</span>)');
+          }
+        }
+        // Otherwise, give them a breakdown of the current, new and removed rows
+        // NOTE: Removed rows include only those that were original and have since been removed.
+        else {
+          var apiReadable = api.readableName.plural;
+          if (numCurrent == 1) {
+            apiReadable = api.readableName.singular;
+          }
+
+          if (numUnsaved != 0 && numRemoved != 0) {
+            return Drupal.t(numCurrent + ' ' + apiReadable + ' (<span class="chado-node-api new">' + numUnsaved + ' new</span>; <span class="chado-node-api removed">' + numRemoved + ' removed</span>)');
+          }
+          else if (numRemoved != 0) {
+            return Drupal.t(numCurrent + ' ' + apiReadable + ' (<span class="chado-node-api removed">' + numRemoved + ' removed</span>)');
+          }
+          else if (numUnsaved != 0) {
+            return Drupal.t(numCurrent + ' ' + apiReadable + ' (<span class="chado-node-api new">' + numUnsaved + ' new</span>)');
+          }
+          else {
+            return Drupal.t(numCurrent + ' ' + apiReadable);
+          }
+        }
+      }
+  }};
+})(jQuery);