From be0dabc043709f525d9ee5d0a4b02cd29980e704 Mon Sep 17 00:00:00 2001
From: Kyle Browning <kylebrowning@me.com>
Date: Wed, 30 Mar 2011 09:37:02 -0700
Subject: [PATCH] Resource UI Update

---
 css/services.admin.css                             |   97 ++++---
 js/services.admin.js                               |  103 ++++++
 .../export_ui/services_ctools_export_ui.class.php  |  330 +++++++-------------
 plugins/export_ui/services_ctools_export_ui.inc    |   12 +-
 services.admin.inc                                 |  114 +++++++
 services.module                                    |    4 +
 6 files changed, 409 insertions(+), 251 deletions(-)
 create mode 100644 js/services.admin.js
 create mode 100644 services.admin.inc

diff --git a/css/services.admin.css b/css/services.admin.css
index fe32067..5f3a53d 100644
--- a/css/services.admin.css
+++ b/css/services.admin.css
@@ -1,37 +1,62 @@
-#services-list-endpoint tr.services-endpoint-disabled {
-  background-color: #fcc;
-  border-color: #ebb;
-}
-#services-list-endpoint tr.services-endpoint-debug {
-  background-color: #ffd;
-  border-color: #eeb;
-}
-#services-list-endpoint tr.services-endpoint-enabled {
-  background-color: #dfd;
-  border-color: #beb;
-}
-#services-help-legend .services-endpoint-disabled {
-  background-color: #fcc;
-  border-color: #ebb;
-  display:inline;
-  padding: 5px;
-}
-#services-help-legend .services-endpoint-debug {
-  background-color: #ffd;
-  border-color: #eeb;
-  display:inline;
-  padding: 5px;
-}
-#services-help-legend .services-endpoint-enabled {
-  background-color: #dfd;
-  border-color: #beb;
-  display:inline;
-  padding: 5px;
-}
-#services-help-legend {
-  position:  relative;
-  display:inline;
-}
-#services-help-legend div{
-  border: 1px solid;
+/* Resource Table */
+#resource-form-table th.select-all {
+  width: 1em;
+}
+th.resource_test {
+  width: 16em;
+}
+
+.resource-image {
+  display: inline-block;
+  cursor: pointer;
+  width: 1em;
+}
+.resource-group-label label {
+  display: inline;
+  font-weight: bold;
+}
+.resource-test-label label {
+  margin-left: 1em; /* LTR */
+}
+.resource-test-description .description {
+  margin: 0;
+}
+#resource-form-table tr td {
+  background-color: white;
+  color: #494949;
+}
+#resource-form-table tr.resource-group td {
+  background-color: #EDF5FA;
+  color: #494949;
+}
+
+table#resource-form-table tr.resource-group label {
+  display: inline;
+}
+
+div.message > div.item-list {
+  font-weight: normal;
+}
+a.resource-collapse {
+  height: 0;
+  width: 0;
+  top: -99em;
+  position: absolute;
+}
+a.resource-collapse:focus,
+a.resource-collapse:hover {
+  font-size: 80%;
+  top: 0px;
+  height: auto;
+  width: auto;
+  overflow: visible;
+  position: relative;
+  z-index: 1000;
+}
+td.resource-group-alias {
+  width:130px;
+}
+/* Zebra colors for resources admin table */
+#resource-form-table tr.resource-even td {
+  background: none repeat scroll 0 0 #F3F4EE;
 }
\ No newline at end of file
diff --git a/js/services.admin.js b/js/services.admin.js
new file mode 100644
index 0000000..892a232
--- /dev/null
+++ b/js/services.admin.js
@@ -0,0 +1,103 @@
+(function ($) {
+
+/**
+ * Add the cool table collapsing on the methoding overview page.
+ */
+Drupal.behaviors.resourceMenuCollapse = {
+  attach: function (context, settings) {
+    var timeout = null;
+    // Adds expand-collapse functionality.
+    $('div.resource-image').each(function () {
+      direction = settings.resource[$(this).attr('id')].imageDirection;
+      $(this).html(settings.resource.images[direction]);
+    });
+
+    // Adds group toggling functionality to arrow images.
+    $('div.resource-image').click(function () {
+      var trs = $(this).parents('tbody').children('.' + settings.resource[this.id].methodClass);
+      var direction = settings.resource[this.id].imageDirection;
+      var row = direction ? trs.size() - 1 : 0;
+
+      // If clicked in the middle of expanding a group, stop so we can switch directions.
+      if (timeout) {
+        clearTimeout(timeout);
+      }
+
+      // Function to toggle an individual row according to the current direction.
+      // We set a timeout of 20 ms until the next row will be shown/hidden to
+      // create a sliding effect.
+      function rowToggle() {
+        if (direction) {
+          if (row >= 0) {
+            $(trs[row]).hide();
+            row--;
+            timeout = setTimeout(rowToggle, 20);
+          }
+        }
+        else {
+          if (row < trs.size()) {
+            $(trs[row]).removeClass('js-hide').show();
+            row++;
+            timeout = setTimeout(rowToggle, 20);
+          }
+        }
+      }
+
+      // Kick-off the toggling upon a new click.
+      rowToggle();
+
+      // Toggle the arrow image next to the method group title.
+      $(this).html(settings.resource.images[(direction ? 0 : 1)]);
+      settings.resource[this.id].imageDirection = !direction;
+
+    });
+  }
+};
+
+/**
+ * Select/deselect all the inner checkboxes when the outer checkboxes are
+ * selected/deselected.
+ */
+Drupal.behaviors.resourceSelectAll = {
+  attach: function (context, settings) {
+    $('td.resource-select-all').each(function () {
+      var methodCheckboxes = settings.resource['resource-method-group-' + $(this).attr('id')].methodNames;
+      var groupCheckbox = $('<input type="checkbox" class="form-checkbox" id="' + $(this).attr('id') + '-select-all" />');
+
+      // Each time a single-method checkbox is checked or unchecked, make sure
+      // that the associated group checkbox gets the right state too.
+      var updateGroupCheckbox = function () {
+        var checkedTests = 0;
+        for (var i = 0; i < methodCheckboxes.length; i++) {
+          $('#' + methodCheckboxes[i]).each(function () {
+            if (($(this).attr('checked'))) {
+              checkedTests++;
+            }
+          });
+        }
+        $(groupCheckbox).attr('checked', (checkedTests == methodCheckboxes.length));
+      };
+
+      // Have the single-method checkboxes follow the group checkbox.
+      groupCheckbox.change(function () {
+        var checked = !!($(this).attr('checked'));
+        for (var i = 0; i < methodCheckboxes.length; i++) {
+          $('#' + methodCheckboxes[i]).attr('checked', checked);
+        }
+      });
+
+      // Have the group checkbox follow the single-method checkboxes.
+      for (var i = 0; i < methodCheckboxes.length; i++) {
+        $('#' + methodCheckboxes[i]).change(function () {
+          updateGroupCheckbox();
+        });
+      }
+
+      // Initialize status for the group checkbox correctly.
+      updateGroupCheckbox();
+      $(this).append(groupCheckbox);
+    });
+  }
+};
+
+})(jQuery);
\ No newline at end of file
diff --git a/plugins/export_ui/services_ctools_export_ui.class.php b/plugins/export_ui/services_ctools_export_ui.class.php
index 930a5e7..e1b689a 100644
--- a/plugins/export_ui/services_ctools_export_ui.class.php
+++ b/plugins/export_ui/services_ctools_export_ui.class.php
@@ -12,7 +12,7 @@ class services_ctools_export_ui extends ctools_export_ui {
    */
   function resources_page($js, $input, $item) {
     drupal_set_title($this->get_page_title('resources', $item));
-    return services_edit_endpoint_resources($item);
+    return drupal_get_form('services_edit_form_endpoint_resources', $item);
   }
 
   /**
@@ -101,7 +101,7 @@ function services_edit_endpoint_resources($endpoint) {
     $endpoint = services_endpoint_load($endpoint);
   }
   if ($endpoint && !empty($endpoint->title)) {
-    drupal_set_title(check_plain($endpoint->title));
+    drupal_set_title($endpoint->title);
   }
   return drupal_get_form('services_edit_form_endpoint_resources', $endpoint);
 }
@@ -122,7 +122,16 @@ function services_edit_form_endpoint_resources(&$form_state, $endpoint) {
     '#type'  => 'value',
     '#value' => $endpoint,
   );
-
+  
+  $form['#attached']['js'] = array(
+    'misc/tableselect.js',
+    drupal_get_path('module', 'services') . '/js/services.admin.js',
+  );
+  
+  $form['#attached']['css'] = array(
+    drupal_get_path('module', 'services') . '/css/services.admin.css',
+  );
+  
   $ops = array(
     'create'   => t('Create'),
     'retrieve' => t('Retrieve'),
@@ -138,84 +147,82 @@ function services_edit_form_endpoint_resources(&$form_state, $endpoint) {
   // are preserved.
   _services_apply_endpoint($resources, $endpoint, FALSE);
 
-  $res = array(
-    '#tree' => TRUE,
-  );
-
-  foreach ($resources as $name => $resource) {
-    $rc = $resource['endpoint'];
-    $res_set = array(
-      '#type'        => 'fieldset',
-      '#title'       => t('!name resource', array(
-        '!name' => preg_replace('/[_-]+/', ' ', $name),
-      )),
-      '#collapsible' => TRUE,
-      '#collapsed'   => TRUE,
-      '#tree'        => TRUE,
-      '#attributes'  => array(
-        'class' => array('resource'),
-      ),
-    );
-
-    $res_set['alias'] = array(
-      '#type'          => 'textfield',
-      '#title'         => t('Alias'),
-      '#description'   => t('The alias you enter here will be used instead of the resource name.'),
-      '#size'          => 40,
-      '#maxlength'     => 255,
-      '#default_value' => isset($rc['alias']) ? $rc['alias'] : '',
+  $form['resources'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('Resources'),
+    '#description' => t('Select the resource(s) or resource group(s) you would like to enable, and click <em>Save</em>.'),
+   );
+
+  $form['resources']['table'] = array(
+    '#theme' => 'services_resource_table',
+   );
+
+  $ignoreArray = array('actions', 'relationships', 'endpoint', 'name', 'file');
+  // Generate the list of methods arranged by resource.
+  foreach ($resources as $resource => $methods) {
+    $form['resources']['table'][$resource] = array(
+      '#collapsed' => TRUE,
     );
 
-    $res_set['operations'] = array(
-      '#tree' => TRUE,
-    );
-    foreach ($ops as $op => $title) {
-      if (isset($resource[$op])) {
-        $res_set['operations'][$op] = array(
-          '#type'        => 'fieldset',
-          '#title'       => $title,
-          '#collapsible' => TRUE,
-          '#collapsed'   => FALSE,
-        );
-        _services_resource_operation_settings($res_set['operations'][$op], $endpoint, $resource, $op);
-      }
+    $alias = '';
+    if (isset($endpoint['build_info']['args'][0]->resources[$resource]['alias'])) {
+      $alias = $endpoint['build_info']['args'][0]->resources[$resource]['alias'];
+    }
+    if (isset($endpoint['input'][$resource . '/alias'])) {
+      $alias = $endpoint['input'][$resource . '/alias'];
     }
 
-    $classes = array(
-      'actions'          => 'actions',
-      'targeted_actions' => 'targeted actions',
-      'relationships'    => 'relationships',
+    $form['resources']['table'][$resource][$resource .'/alias'] = array(
+      '#type' => 'textfield',
+      '#default_value' => $alias,
+      '#name' => $resource .'/alias',
+      '#size' => 20,
     );
-    foreach ($classes as $element => $class) {
-      if (!empty($resource[$class])) {
-        $res_set[$element] = array(
-          '#type'  => 'fieldset',
-          '#title' => t($class),
-          '#tree'  => TRUE,
-        );
-        foreach ($resource[$class] as $action => $definition) {
-          $res_set[$element][$action] = array(
-            '#type'        => 'fieldset',
-            '#title'       => $action,
-            '#collapsible' => TRUE,
-            '#collapsed'   => FALSE,
-          );
-          _services_resource_operation_settings($res_set[$element][$action], $endpoint, $resource, $class, $action);
+    foreach ($methods as $class => $info) {
+      if(!in_array($class, $ignoreArray)) {
+        if(!isset($info['help'])) {
+          $description = t('No description is available');
+        } else {
+          $description = $info['help'];
         }
-      }
-    }
-
-    drupal_alter('services_resource_settings', $res_set, $resource);
-
-    $res[$name] = $res_set;
-  }
-
-  $form['resources'] = $res;
-
-  $form['save'] = array(
-    '#type'  => 'submit',
-    '#value' => t('Save'),
-  );
+        if (isset($endpoint['build_info']['args'][0]->resources[$resource]['operations'][$class])) {
+          $default_value = $endpoint['build_info']['args'][0]->resources[$resource]['operations'][$class]['enabled'];
+        } else {
+          $default_value = 0;
+        }
+        $form['resources']['table'][$resource][$resource .'/'. $class] = array(
+          '#type' => 'checkbox',
+          '#title' => $class,
+          '#description' => $description,
+          '#default_value' => $default_value,
+         );
+      } else if($class == 'actions' || $class == 'relationships' || $class == 'targeted_actions') {
+        foreach($info as $key=>$action) {
+          if(!isset($action['help'])) {
+            $description = t('No description is available');
+          } else {
+            $description = $action['help'];
+          }
+          if (isset($endpoint['build_info']['args'][0]->resources[$resource][$class][$key])) {
+            $default_value = $endpoint['build_info']['args'][0]->resources[$resource][$class][$key]['enabled'];
+          } else {
+            $default_value = 0;
+          }
+          $form['resources']['table'][$resource][$resource .'/'. $key .'/'. $class] = array(
+            '#type' => 'checkbox',
+            '#title' => $key,
+            '#description' => $description,
+            '#default_value' => $default_value,
+          );
+         }
+       }
+     }
+   }
+
+   $form['save'] = array(
+     '#type'  => 'submit',
+     '#value' => t('Save'),
+   );
   return $form;
 }
 
@@ -227,157 +234,56 @@ function services_edit_form_endpoint_resources(&$form_state, $endpoint) {
  * @return void
  */
 function services_edit_form_endpoint_resources_validate($form, $form_state) {
-  $res = $form_state['values']['resources'];
-  // Validate aliases
-  foreach ($res as $name => $resource) {
-    if (!empty($resource['alias'])) {
-      if (!preg_match('/^[a-z-]+$/', $resource['alias'])) {
-        form_set_error("resources][{$name}][alias", t("The alias for the !name may only contain lower case a-z and dashes.", array(
-          '!name' => $form['resources'][$name]['#title'],
-        )));
-      }
+  $input = $form_state['values']['endpoint_object']['input'];
+
+  // Validate aliases.
+  foreach ($input as $key => $value) {
+    if (strpos($key, '/alias') !== FALSE && !empty($value) && !preg_match('/^[a-z-]+$/', $value)) {
+      list($resource_name,) = explode('/', $key);
+      // Still this doesn't highlight needed form element.
+      form_error($form['resources']['table'][$resource_name][$resource_name. '/alias'], t("The alias for the !name resource may only contain lower case a-z and dashes.", array(
+        '!name' => $resource_name,
+      )));
     }
   }
 }
 
 /**
- * services_edit_form_endpoint_resources_submit function.
+ * Resources form submit function..
  *
  * @param array $form
  * @param array $form_state
  * @return void
  */
 function services_edit_form_endpoint_resources_submit($form, $form_state) {
-  $resources = $form_state['input']['resources'];
-  $endpoint  = $form_state['build_info']['args'][0];
-  //Loop through submitted resources and build the final resource for submission
-  foreach ($resources as $name => $resource) {
-    $used = FALSE;
-    $final_resource = isset($endpoint->resources[$name]) ? $endpoint->resources[$name] : array();
-    //Add the alias that was submitted
-    $final_resource['alias'] = $resource['alias'];
-    //Check if operations were submitted
-    if (isset($resource['operations'])) {
-      //Loop through operations that were submitted and add them to our final resource structure
-      foreach ($resource['operations'] as $op => $def) {
-        $cop = isset($final_resource['operations'][$op]) ? $final_resource['operations'][$op] : array();
-        //take old value and overwrite it
-        $cop = array_merge($cop, $def);
-        //If were still enabled set the operation method to enabled
-        if ($cop['enabled']) {
-          $final_resource['operations'][$op] = $cop;
-          $used = $used || TRUE;
-        }
-        //Since it wasnt used, IE, null unset this value
-        else {
-          unset($final_resource['operations'][$op]);
-        }
-      }
-    }
-    $classes = array(
-      'actions' => 'actions',
-      'targeted_actions' => 'targeted actions',
-      'relationships' => 'relationships',
-    );
-    foreach ($classes as $element => $class) {
-      $class_used = FALSE;
-      if (!empty($resource[$element])) {
-        foreach ($resource[$element] as $act => $def) {
-          $cop = isset($final_resource[$class][$act]) ? $final_resource[$class][$act] : array();
-          $cop = array_merge($cop, $def);
-          if ($cop['enabled']) {
-            $final_resource[$class][$act] = $cop;
-            $class_used = $class_used || TRUE;
-          }
-          else {
-            unset($final_resource[$class][$act]);
-          }
-        }
-        if (!$class_used) {
-          unset($final_resource[$class]);
-        }
-        $used = $class_used || $used;
-      }
-    }
-
-    if ($used) {
-      $endpoint->resources[$name] = $final_resource;
-    }
-    else {
-      unset($endpoint->resources[$name]);
-    }
-  }
-  drupal_set_message(t('Your resources have been saved.'));
-  services_endpoint_save($endpoint);
-}
-
-/**
- * Returns information about a resource operation given it's class and name.
- *
- * @return array
- *  Information about the operation, or NULL if no matching
- *  operation was found.
- */
-function services_get_resource_operation_info($resource, $class, $name = NULL) {
-  $op = NULL;
-
-  if (isset($resource[$class])) {
-    $op = $resource[$class];
-    if (!empty($name)) {
-      $op = isset($op[$name]) ? $op[$name] : NULL;
-    }
-  }
-
-  return $op;
-}
-/**
- * Constructs the settings form for resource operation.
- *
- * @param array $settings
- *  The root element for the settings form.
- * @param string $resource
- *  The resource information array.
- * @param string $class
- *  The class of the operation. Can be 'create', 'retrieve', 'update',
- *  'delete', 'index', 'actions' or 'targeted actions' or 'relationships'.
- * @param string $name
- *  Optional. The name parameter is only used for actions, targeted actions
- *  and relationship.
- */
-function _services_resource_operation_settings(&$settings, $endpoint, $resource, $class, $name = NULL) {
-  module_load_include('runtime.inc', 'services');
-  if ($rop = services_get_resource_operation_info($resource, $class, $name)) {
-    $settings['enabled'] = array(
-      '#type' => 'checkbox',
-      '#title' => t('Enabled'),
-      '#default_value' => !empty($rop['endpoint']) && $rop['endpoint']['enabled'],
-    );
+  $endpoint  = $form_state['values']['endpoint_object'];
+  $existing_resources = _services_build_resources();
+  // Apply the endpoint in a non-strict mode, so that the non-active resources
+  // are preserved.
+  _services_apply_endpoint($existing_resources, $endpoint, FALSE);
+  $resources = $endpoint['input'];
+  $endpoint = $endpoint['build_info']['args'][0];
 
-    if (!empty($rop['endpoint']['preprocess'])) {
-      $settings['preprocess'] = array(
-        '#type' => 'item',
-        '#title' => t('Preprocess function'),
-        '#value' => $rop['endpoint']['preprocess'],
-      );
+  foreach ($resources as $path => $state) {
+    if (strpos($path, '/') === FALSE || empty($state)) {
+      continue;
     }
-
-    if (!empty($rop['endpoint']['postprocess'])) {
-      $settings['preprocess'] = array(
-        '#type' => 'item',
-        '#title' => t('Postprocess function'),
-        '#value' => $rop['endpoint']['Postprocess'],
-      );
+    $split_path = explode('/', $path);
+    $resource = $split_path[0];
+    $method = $split_path[1];
+    // If method is alias.
+    if ($method == 'alias') {
+      $final_resource[$resource]['alias'] = $state;
+      continue;
     }
-
-    // Let authentication modules add their configuration options
-    if(isset($endpoint->authentication))
-    foreach ($endpoint->authentication as $auth_module => $auth_settings) {
-      $settings_form = services_auth_invoke($auth_module, 'controller_settings', $auth_settings, $rop, $endpoint, $class, $name);
-      if (!empty($settings_form)) {
-        $settings[$auth_module] = $settings_form;
-      }
+    // If it is action, relationship, or targeted action.
+    if (isset($split_path[2])) {
+      $final_resource[$resource][$split_path[2]][$method]['enabled'] = 1;
+      continue;
     }
-
-    drupal_alter('services_resource_operation_settings', $settings, $endpoint, $resource, $class, $name);
+    // If it is operation.
+    $final_resource[$resource]['operations'][$method]['enabled'] = 1;
   }
+  $endpoint->resources = $final_resource;
+  services_endpoint_save($endpoint);
 }
diff --git a/plugins/export_ui/services_ctools_export_ui.inc b/plugins/export_ui/services_ctools_export_ui.inc
index 1d23e58..06fd363 100644
--- a/plugins/export_ui/services_ctools_export_ui.inc
+++ b/plugins/export_ui/services_ctools_export_ui.inc
@@ -17,6 +17,7 @@ $plugin = array(
         'load arguments' => array('services_ctools_export_ui'),
         'access arguments' => array('administer services'),
         'type' => MENU_LOCAL_TASK,
+
       ),
       'authentication' => array(
         'path' => 'list/%ctools_export_ui/authentication',
@@ -26,15 +27,20 @@ $plugin = array(
         'load arguments' => array('services_ctools_export_ui'),
         'access arguments' => array('administer services'),
         'type' => MENU_LOCAL_TASK,
+        'weight' => -1,
       ),
     ),
   ),
   // Add our custom operations.
   'allowed operations' => array(
-    'resources'  => array('title' => t('Resources')),
-    'authentication' => array('title' => t('Authentication')),
+    'resources'  => array('title' => t('Edit Resources')),
+    'authentication' => array('title' => t('Edit Authentication')),
+  ),
+  'form' => array(
+    'settings' => 'services_ctools_export_ui_form',
+    'validate' => 'services_ctools_export_ui_form_validate',
+    'submit' => 'services_ctools_export_ui_form_submit',
   ),
-
   'handler' => array(
     'class' => 'services_ctools_export_ui',
     'parent' => 'ctools_export_ui',
diff --git a/services.admin.inc b/services.admin.inc
new file mode 100644
index 0000000..240b8b7
--- /dev/null
+++ b/services.admin.inc
@@ -0,0 +1,114 @@
+<?php
+function theme_services_resource_table($variables) {
+  $table = $variables['table'];
+
+  // Create header for resource selection table.
+  $header = array(
+    array('class' => array('select-all')),
+    array('data' => t('Resource'), 'class' => array('resource_method')),
+    array('data' => t('Description'), 'class' => array('resource_description')),
+    array('data' => t('Alias'), 'class' => array('resource_alias')),
+  );
+
+  // Define the images used to expand/collapse the method groups.
+  $js = array(
+    'images' => array(
+      theme('image', array('path' => 'misc/menu-collapsed.png', 'alt' => t('Expand'), 'title' => t('Expand'))) . ' <a href="#" class="resource-collapse">(' . t('Expand') . ')</a>',
+      theme('image', array('path' => 'misc/menu-expanded.png', 'alt' => t('Collapse'), 'title' => t('Collapse'))) . ' <a href="#" class="resource-collapse">(' . t('Collapse') . ')</a>',
+    ),
+  );
+
+  // Cycle through each method group and create a row.
+  $rows = array();
+  foreach (element_children($table) as $key) {
+    $element = &$table[$key];
+    $row = array();
+
+    // Make the class name safe for output on the page by replacing all
+    // non-word/decimal characters with a dash (-).
+    $method_class = strtolower(trim(preg_replace("/[^\w\d]/", "-", $key)));
+
+    // Select the right "expand"/"collapse" image, depending on whether the
+    // category is expanded (at least one method selected) or not.
+    $collapsed = !empty($element['#collapsed']);
+    $image_index = $collapsed ? 0 : 1;
+
+    // Place-holder for checkboxes to select group of methods.
+    $row[] = array('id' => $method_class, 'class' => array('resource-select-all'));
+
+    // Expand/collapse image and group title.
+    $row[] = array(
+      'data' => '<div class="resource-image" id="resource-method-group-' . $method_class . '"></div>' .
+        '<label for="' . $method_class . '-select-all" class="resource-group-label">' . $key . '</label>',
+      'class' => array('resource-group-label'),
+    );
+    $row[] = array(
+      'data' => '&nbsp;',
+      'class' => array('resource-group-description'),
+    );
+    $row[] = array(
+      'data' => drupal_render($element[$method_class.'/alias']),
+      'class' => array('resource-group-alias'),
+    );
+    $rows[] = array('data' => $row, 'class' => array('resource-group'));
+
+    // Add individual methods to group.
+    $current_js = array(
+      'methodClass' => $method_class . '-method',
+      'methodNames' => array(),
+      'imageDirection' => $image_index,
+      'clickActive' => FALSE,
+    );
+
+    // Cycle through each method within the current group.
+    foreach (element_children($element) as $method_name) {
+      if(strpos($method_name, 'alias') === FALSE) {
+        $method = $element[$method_name];
+        $row = array();
+
+        $current_js['methodNames'][] = $method['#id'];
+
+        // Store method title and description so that checkbox won't render them.
+        $title = $method['#title'];
+        $description = $method['#description'];
+
+        $method['#title_display'] = 'invisible';
+        unset($method['#description']);
+
+        // Test name is used to determine what methods to run.
+        $method['#name'] = $method_name;
+
+        $row[] = array(
+          'data' => drupal_render($method),
+          'class' => array('resource-method-select'),
+        );
+        $row[] = array(
+          'data' => '<label for="' . $method['#id'] . '">' . $title . '</label>',
+          'class' => array('resource-method-label'),
+        );
+        $row[] = array(
+          'data' => '<div class="description">' . $description . '</div>',
+          'class' => array('resource-method-description'),
+        );
+        $row[] = array(
+          'data' => '<div class="alias">&nbsp;</div>',
+          'class' => array('resource-method-alias'),
+        );
+        $rows[] = array('data' => $row, 'class' => array($method_class . '-method', ($collapsed ? 'js-hide' : '')));
+      }
+
+    }
+    $js['resource-method-group-' . $method_class] = $current_js;
+    unset($table[$key]);
+  }
+
+  // Add js array of settings.
+  drupal_add_js(array('resource' => $js), 'setting');
+
+  if (empty($rows)) {
+    return '<strong>' . t('No resourcess to display.') . '</strong>';
+  }
+  else {
+    return theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => array('id' => 'resource-form-table')));
+  }
+}
\ No newline at end of file
diff --git a/services.module b/services.module
index 1ca1649..0c50698 100644
--- a/services.module
+++ b/services.module
@@ -139,6 +139,10 @@ function services_theme() {
       'template'  => 'services_endpoint_index',
       'arguments' => array('endpoints' => NULL),
     ),
+    'services_resource_table' => array(
+     'render element' => 'table',
+      'file' => 'services.admin.inc',
+    ),
   );
 }
 
-- 
1.7.3.4

