From 9ab3fe900a074ecfc6ff180a72c752c404a5b3c6 Mon Sep 17 00:00:00 2001
From: ygerasimov <yuri.gerasimov@gmail.com>
Date: Sat, 19 Mar 2011 18:59:40 +0200
Subject: [PATCH] Issue #1096316 by kylebrowning, ygerasimov: Updates Resources UI to a way better interface. Validation to resources alias field. Zebra colors to resources page.

---
 css/services.admin.css                             |   99 +++--
 js/services.admin.js                               |  103 +++++
 .../export_ui/services_ctools_export_ui.class.php  |  301 +++++---------
 plugins/export_ui/services_ctools_export_ui.inc    |   59 +--
 services.admin.inc                                 |  466 ++++++--------------
 services.module                                    |    4 +
 6 files changed, 421 insertions(+), 611 deletions(-)
 create mode 100644 js/services.admin.js

diff --git a/css/services.admin.css b/css/services.admin.css
index fe32067..b6ce83c 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;
-}
\ No newline at end of file
+/* Test 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;
+}
diff --git a/js/services.admin.js b/js/services.admin.js
new file mode 100644
index 0000000..cf84634
--- /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);
diff --git a/plugins/export_ui/services_ctools_export_ui.class.php b/plugins/export_ui/services_ctools_export_ui.class.php
index ec6e77e..aa00812 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);
   }
 
   /**
@@ -77,6 +77,9 @@ function services_edit_form_endpoint_authentication($form, &$form_state) {
   return $form;
 }
 
+/**
+ * Submit function for endpoint authentication configuration form.
+ */
 function services_edit_form_endpoint_authentication_submit($form, $form_state) {
   $endpoint = $form_state['values']['endpoint_object'];
 
@@ -88,7 +91,6 @@ function services_edit_form_endpoint_authentication_submit($form, $form_state) {
   services_endpoint_save($endpoint);
 }
 
-
 /**
  * services_edit_endpoint_resources function.
  *
@@ -101,7 +103,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);
 }
@@ -115,14 +117,12 @@ function services_edit_endpoint_resources($endpoint) {
  */
 function services_edit_form_endpoint_resources(&$form_state, $endpoint) {
   module_load_include('resource_build.inc', 'services');
-
   $form = array();
 
   $form['endpoint_object'] = array(
     '#type'  => 'value',
     '#value' => $endpoint,
   );
-
   $ops = array(
     'create'   => t('Create'),
     'retrieve' => t('Retrieve'),
@@ -137,80 +137,81 @@ function services_edit_form_endpoint_resources(&$form_state, $endpoint) {
   // Apply the endpoint in a non-strict mode, so that the non-active resources
   // are preserved.
   _services_apply_endpoint($resources, $endpoint, FALSE);
-
-  $res = array(
-    '#tree' => TRUE,
+  $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',
   );
-
-  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'),
-      ),
+  $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['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'] : '',
-    );
+    $alias = '';
+    if (isset($endpoint['build_info']['args'][0]->resources[$resource]['alias'])) {
+      $alias = $endpoint['build_info']['args'][0]->resources[$resource]['alias'];
+    }
 
-    $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);
-      }
+    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]['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)) {
+
+        $description = t('No description is available');
+        if (isset($info['help'])) {
+          $description = $info['help'];
+        }
+
+        $default_value = 0;
+        if (isset($endpoint['build_info']['args'][0]->resources[$resource]['operations'][$class])) {
+          $default_value = $endpoint['build_info']['args'][0]->resources[$resource]['operations'][$class]['enabled'];
         }
+
+        $form['resources']['table'][$resource][$resource .'/'. $class] = array(
+          '#type' => 'checkbox',
+          '#title' => $class,
+          '#description' => $description,
+          '#default_value' => $default_value,
+         );
       }
-    }
+      elseif ($class == 'actions' || $class == 'relationships' || $class == 'targeted_actions') {
+        foreach ($info as $key => $action) {
 
-    drupal_alter('services_resource_settings', $res_set, $resource);
+          $description = t('No description is available');
+          if (isset($action['help'])) {
+            $description = $action['help'];
+          }
 
-    $res[$name] = $res_set;
-  }
+          $default_value = 0;
+          if (isset($endpoint['build_info']['args'][0]->resources[$resource][$class][$key])) {
+            $default_value = $endpoint['build_info']['args'][0]->resources[$resource][$class][$key]['enabled'];
+          }
 
-  $form['resources'] = $res;
+          $form['resources']['table'][$resource][$resource .'/'. $key .'/'. $class] = array(
+            '#type' => 'checkbox',
+            '#title' => $key,
+            '#description' => $description,
+            '#default_value' => $default_value,
+          );
+        }
+      }
+    }
+  }
 
   $form['save'] = array(
     '#type'  => 'submit',
@@ -227,155 +228,57 @@ 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_set_error("resources][table][$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['values']['resources'];
   $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];
 
-  foreach ($resources as $name => $resource) {
-    $used = FALSE;
-    $c = isset($endpoint->resources[$name]) ? $endpoint->resources[$name] : array();
-
-    $c['alias'] = $resource['alias'];
-    if (isset($resource['operations'])) {
-      foreach ($resource['operations'] as $op => $def) {
-        $cop = isset($c['operations'][$op]) ? $c['operations'][$op] : array();
-        $cop = array_merge($cop, $def);
-        if ($cop['enabled']) {
-          $c['operations'][$op] = $cop;
-          $used = $used || TRUE;
-        }
-        else {
-          unset($c['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($c[$class][$act]) ? $c[$class][$act] : array();
-          $cop = array_merge($cop, $def);
-          if ($cop['enabled']) {
-            $c[$class][$act] = $cop;
-            $class_used = $class_used || TRUE;
-          }
-          else {
-            unset($c[$class][$act]);
-          }
-        }
-        if (!$class_used) {
-          unset($c[$class]);
-        }
-        $used = $class_used || $used;
-      }
+  foreach ($resources as $path => $state) {
+    if (strpos($path, '/') === FALSE || empty($state)) {
+      continue;
     }
-
-    if ($used) {
-      $endpoint->resources[$name] = $c;
+    $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;
     }
-    else {
-      unset($endpoint->resources[$name]);
+    // If it is action, relationship, or targeted action.
+    if (isset($split_path[2])) {
+      $final_resource[$resource][$split_path[2]][$method]['enabled'] = 1;
+      continue;
     }
+    // If it is operation.
+    $final_resource[$resource]['operations'][$method]['enabled'] = 1;
   }
+  $endpoint->resources = $final_resource;
 
-  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'],
-    );
-
-    if (!empty($rop['endpoint']['preprocess'])) {
-      $settings['preprocess'] = array(
-        '#type' => 'item',
-        '#title' => t('Preprocess function'),
-        '#value' => $rop['endpoint']['preprocess'],
-      );
-    }
-
-    if (!empty($rop['endpoint']['postprocess'])) {
-      $settings['preprocess'] = array(
-        '#type' => 'item',
-        '#title' => t('Postprocess function'),
-        '#value' => $rop['endpoint']['Postprocess'],
-      );
-    }
-
-    // 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;
-      }
-    }
-
-    drupal_alter('services_resource_operation_settings', $settings, $endpoint, $resource, $class, $name);
-  }
-}
diff --git a/plugins/export_ui/services_ctools_export_ui.inc b/plugins/export_ui/services_ctools_export_ui.inc
index 276b499..f7f5f5b 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,
+        'weight' => -1,
       ),
       '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',
@@ -65,16 +71,6 @@ function services_ctools_export_ui_form(&$form, &$form_state) {
     '#value' => $endpoint,
   );
 
-  $form['name'] = array(
-    '#type'          => 'textfield',
-    '#size'          => 24,
-    '#maxlength'     => 255,
-    '#default_value' => $endpoint->name,
-    '#title'         => t('Endpoint name'),
-    '#description'   => t('A unique name used to identify this preset internally. It must be only be alpha characters and underscores. No spaces, numbers or uppercase characters.'),
-    '#required'      => TRUE,
-  );
-
   $form['title'] = array(
     '#type'          => 'textfield',
     '#size'          => 24,
@@ -138,13 +134,8 @@ function services_ctools_export_ui_form(&$form, &$form_state) {
   }
 
   $label = (empty($endpoint->eid) && $endpoint->export_type != EXPORT_IN_CODE) ? t('Save and proceed') : t('Save');
-  $form['submit'] = array(
-    '#type'  => 'submit',
-    '#value' => $label,
-  );
 
   return $form;
-
 }
 
 /**
@@ -153,7 +144,7 @@ function services_ctools_export_ui_form(&$form, &$form_state) {
 function services_ctools_export_ui_form_validate(&$form, &$form_state) {
   // Test uniqueness of name:
   if (preg_match("/[^A-Za-z0-9_]/", $form_state['values']['name'])) {
-    form_error($form['name'], t('Endpoint name must be alphanumeric or underscores only.'));
+    form_error($form['info']['name'], t('Endpoint name must be alphanumeric or underscores only.'));
   }
   else {
     $query = db_select('services_endpoint', 'e');
@@ -165,8 +156,9 @@ function services_ctools_export_ui_form_validate(&$form, &$form_state) {
     }
 
     $res = $query->execute()->fetchField();
-    if (!empty($res)) {
-      form_error($form['name'], t('Endpoint name must be unique.'));
+    if (is_string($res)) {
+      form_error($form['info']['name'], t('Endpoint name must be unique.'));
+      return;
     }
   }
   //TODO: More validation? Eg. validate path? Transliteration etc?
@@ -178,33 +170,22 @@ function services_ctools_export_ui_form_validate(&$form, &$form_state) {
 function services_ctools_export_ui_form_submit(&$form, &$form_state) {
   $endpoint = $form_state['values']['endpoint_object'];
 
-  $endpoint->name           = $form_state['values']['name'];
-  $endpoint->title          = $form_state['values']['title'];
-  $endpoint->server         = $form_state['values']['server'];
-  $endpoint->path           = $form_state['values']['path'];
+  $endpoint->name   = $form_state['values']['name'];
+  $endpoint->title  = $form_state['values']['title'];
+  $endpoint->server = $form_state['values']['server'];
+  $endpoint->path   = $form_state['values']['path'];
 
   // Set the authentication modules, and preserve the settings for modules
   // that already exist.
   $auth = array();
   if (isset($form_state['values']['authentication'])) {
     foreach (array_keys($form_state['values']['authentication']) as $module) {
+      $auth[$module] = array();
       if (isset($endpoint->authentication[$module])) {
         $auth[$module] = $endpoint->authentication[$module];
       }
-      else {
-        $auth[$module] = array();
-      }
     }
   }
   $endpoint->authentication = $auth;
-
-  if (empty($endpoint->eid)) {
-    drupal_set_message(t('Your new endpoint %title has been saved.', array('%title' => $endpoint->title)));
-    services_endpoint_save($endpoint);
-    $form_state['values']['eid'] = $endpoint->eid;
-  }
-  else {
-    drupal_set_message(t('Your changes have been saved.'));
-    services_endpoint_save($endpoint);
-  }
-}
\ No newline at end of file
+  $form_state['values']['endpoint_object'] = $endpoint;
+}
diff --git a/services.admin.inc b/services.admin.inc
index 6c1fca6..1d42d82 100644
--- a/services.admin.inc
+++ b/services.admin.inc
@@ -6,6 +6,133 @@
  *
  * This provides the UI to list, create, edit and delete endpoints.
  */
+/**
+ * Returns HTML for an endpoint generated by services_edit_form_endpoint_resources() into a table.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - table: A render element representing the table.
+ *
+ * @ingroup themeable
+ */
+function theme_services_resource_table($variables) {
+  $table = $variables['table'];
+
+  drupal_add_css(drupal_get_path('module', 'services') . '/css/services.admin.css');
+  drupal_add_js(drupal_get_path('module', 'services') . '/js/services.admin.js');
+  drupal_add_js('misc/tableselect.js');
+
+  // 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();
+  $i = 0;
+  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['alias']),
+      'class' => array('resource-group-alias'),
+    );
+    $resource_class = ($i++ % 2 == 1) ? 'resource-even' : 'resource-odd';
+    $rows[] = array('data' => $row, 'class' => array('resource-group', $resource_class));
+
+    // 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 ($method_name != 'alias') {
+        $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>';
+  }
+  return theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => array('id' => 'resource-form-table')));
+}
 
 /**
  * Output a list of endpoints.
@@ -66,7 +193,7 @@ function services_list_endpoint($js = NULL) {
     elseif ($endpoint->export_type != EXPORT_IN_CODE) {
       $operations[] = array(
         'title' => t('Delete'),
-        'href'  => 'admin/config/services/services/'. $endpoint->name . '/delete',
+        'href'  => 'admin/config/services/services/' . $endpoint->name . '/delete',
       );
     }
     else {
@@ -90,7 +217,7 @@ function services_list_endpoint($js = NULL) {
       else {
         $row_class = 'services-endpoint-enabled';
       }
-     } 
+     }
      else {
        $row_class = 'services-endpoint-disabled';
      }
@@ -468,337 +595,4 @@ function services_edit_endpoint_authentication($endpoint) {
     drupal_set_title(check_plain($endpoint->title));
   }
   return drupal_get_form('services_edit_form_endpoint_authentication', $endpoint);
-}
-
-/**
- * Endpoint authentication configuration form.
- */
-function services_edit_form_endpoint_authentication($form, &$form_state) {
-  list($endpoint) = $form_state['build_info']['args'];
-  // Loading runtime include as needed by services_authentication_info().
-  module_load_include('runtime.inc', 'services');
-
-  $auth_modules = module_implements('services_authentication_info');
-
-  $form['endpoint_object'] = array(
-    '#type'  => 'value',
-    '#value' => $endpoint,
-  );
-
-  if (empty($auth_modules)) {
-    $form['message'] = array(
-      '#type'          => 'item',
-      '#title'         => t('No installed authentication modules'),
-      '#description'   => t('No authentication modules are installed, standard ' .
-        'Drupal session based security will be used.'),
-    );
-  }
-  elseif (empty($endpoint->authentication)) {
-    $form['message'] = array(
-      '#type'          => 'item',
-      '#title'         => t('No enabled authentication modules'),
-      '#value'   => t('No authentication modules are enabled, standard ' .
-        'Drupal session based security will be used.'),
-    );
-  }
-  else {
-    // Add configuration fieldsets for the authentication modules
-    foreach ($endpoint->authentication as $module => $settings) {
-      $info = services_authentication_info($module);
-      if ($info) {
-        $form[$module] = array(
-          '#type' => 'fieldset',
-          '#title' => $info['title'],
-          '#tree' => TRUE,
-        ) + services_auth_invoke($module, 'security_settings', $settings);
-      }
-    }
-  }
-
-  $form['submit'] = array(
-    '#type'  => 'submit',
-    '#value' => 'Save',
-  );
-
-  return $form;
-}
-
-function services_edit_form_endpoint_authentication_submit($form, $form_state) {
-  $endpoint = $form_state['values']['endpoint_object'];
-
-  foreach (array_keys($endpoint->authentication) as $module) {
-    $endpoint->authentication[$module] = $form_state['values'][$module];
-  }
-
-  drupal_set_message(t('Your authentication options have been saved.'));
-  services_endpoint_save($endpoint);
-}
-
-/**
- * Add resources to an endpoint.
- */
-function services_edit_endpoint_resources($endpoint) {
-  if (!is_object($endpoint)) {
-    $endpoint = services_endpoint_load($endpoint);
-  }
-  if ($endpoint && !empty($endpoint->title)) {
-    drupal_set_title(check_plain($endpoint->title));
-  }
-  return drupal_get_form('services_edit_form_endpoint_resources', $endpoint);
-}
-
-/**
- * Form to add resources to an endpoint.
- */
-function services_edit_form_endpoint_resources($form, &$form_state) {
-  list($endpoint) = $form_state['build_info']['args'];
-  module_load_include('resource_build.inc', 'services');
-
-  $form['endpoint_object'] = array(
-    '#type'  => 'value',
-    '#value' => $endpoint,
-  );
-
-  $ops = array(
-    'create'   => t('Create'),
-    'retrieve' => t('Retrieve'),
-    'update'   => t('Update'),
-    'delete'   => t('Delete'),
-    'index'    => t('Index'),
-  );
-
-  // Call _services_build_resources() directly instead of
-  // services_get_resources to bypass caching.
-  $resources = _services_build_resources();
-  // Apply the endpoint in a non-strict mode, so that the non-active resources
-  // 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'] : '',
-    );
-
-    $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);
-      }
-    }
-
-    $classes = array(
-      'actions'          => 'actions',
-      'targeted_actions' => 'targeted actions',
-      'relationships'    => 'relationships',
-    );
-    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);
-        }
-      }
-    }
-
-    drupal_alter('services_resource_settings', $res_set, $resource);
-
-    $res[$name] = $res_set;
-  }
-
-  $form['resources'] = $res;
-
-  $form['save'] = array(
-    '#type'  => 'submit',
-    '#value' => t('Save'),
-  );
-
-  return $form;
-}
-
-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'],
-        )));
-      }
-    }
-  }
-}
-
-function services_edit_form_endpoint_resources_submit($form, $form_state) {
-  $resources = $form_state['values']['resources'];
-  $endpoint  = $form_state['values']['endpoint_object'];
-
-  foreach ($resources as $name => $resource) {
-    $used = FALSE;
-    $c = isset($endpoint->resources[$name]) ? $endpoint->resources[$name] : array();
-
-    $c['alias'] = $resource['alias'];
-    if (isset($resource['operations'])) {
-      foreach ($resource['operations'] as $op => $def) {
-        $cop = isset($c['operations'][$op]) ? $c['operations'][$op] : array();
-        $cop = array_merge($cop, $def);
-        if ($cop['enabled']) {
-          $c['operations'][$op] = $cop;
-          $used = $used || TRUE;
-        }
-        else {
-          unset($c['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($c[$class][$act]) ? $c[$class][$act] : array();
-          $cop = array_merge($cop, $def);
-          if ($cop['enabled']) {
-            $c[$class][$act] = $cop;
-            $class_used = $class_used || TRUE;
-          }
-          else {
-            unset($c[$class][$act]);
-          }
-        }
-        if (!$class_used) {
-          unset($c[$class]);
-        }
-        $used = $class_used || $used;
-      }
-    }
-
-    if ($used) {
-      $endpoint->resources[$name] = $c;
-    }
-    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'],
-    );
-
-    if (!empty($rop['endpoint']['preprocess'])) {
-      $settings['preprocess'] = array(
-        '#type' => 'item',
-        '#title' => t('Preprocess function'),
-        '#value' => $rop['endpoint']['preprocess'],
-      );
-    }
-
-    if (!empty($rop['endpoint']['postprocess'])) {
-      $settings['preprocess'] = array(
-        '#type' => 'item',
-        '#title' => t('Postprocess function'),
-        '#value' => $rop['endpoint']['Postprocess'],
-      );
-    }
-
-    // Let authentication modules add their configuration options
-    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;
-      }
-    }
-
-    drupal_alter('services_resource_operation_settings', $settings, $endpoint, $resource, $class, $name);
-  }
-}
+}
\ No newline at end of file
diff --git a/services.module b/services.module
index ede584b..cdc1c27 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.0.4

