array(
'IE' => 'lte IE 7',
'!IE' => FALSE,
),
'preprocess' => FALSE,
);
$list[$module_path . '/css/views-admin.theme.css'] = array();
// Add in any theme specific CSS files we have.
$themes = list_themes();
$theme_key = $GLOBALS['theme'];
while ($theme_key) {
// Try to find the admin css file for non-core themes.
if (!in_array($theme_key, array('garland', 'seven', 'bartik'))) {
$theme_path = drupal_get_path('theme', $theme_key);
// First search in the css directory, then in the root folder of the
// theme.
if (file_exists($theme_path . "/css/views-admin.$theme_key.css")) {
$list[$theme_path . "/css/views-admin.$theme_key.css"] = array(
'group' => CSS_THEME,
);
}
elseif (file_exists($theme_path . "/views-admin.$theme_key.css")) {
$list[$theme_path . "/views-admin.$theme_key.css"] = array(
'group' => CSS_THEME,
);
}
}
else {
$list[$module_path . "/css/views-admin.$theme_key.css"] = array(
'group' => CSS_THEME,
);
}
$theme_key = isset($themes[$theme_key]->base_theme) ? $themes[$theme_key]->base_theme : '';
}
// Views contains style overrides for the following modules.
$module_list = array('contextual', 'advanced_help', 'ctools');
foreach ($module_list as $module) {
if (module_exists($module)) {
$list[$module_path . '/css/views-admin.' . $module . '.css'] = array();
}
}
return $list;
}
/**
* Adds standard Views administration CSS to the current page.
*/
function views_ui_add_admin_css() {
foreach (views_ui_get_admin_css() as $file => $options) {
drupal_add_css($file, $options);
}
}
/**
* Check to see if the advanced help module is installed.
*
* If not display a message.
*
* Only call this function if the user is already in a position for this to be
* useful.
*/
function views_ui_check_advanced_help() {
if (!variable_get('views_ui_show_advanced_help_warning', TRUE)) {
return;
}
if (!module_exists('advanced_help')) {
$filename = db_query_range("SELECT filename FROM {system} WHERE type = 'module' AND name = 'advanced_help'", 0, 1)
->fetchField();
if ($filename && file_exists($filename)) {
drupal_set_message(t('If you enable the advanced help module, Views will provide more and better help. You can disable this message at the Views settings page.',
array(
'@modules' => url('admin/modules'),
'@hide' => url('admin/structure/views/settings'),
)));
}
else {
drupal_set_message(t('If you install the advanced help module from !href, Views will provide more and better help. You can disable this message at the Views settings page.',
array(
'!href' => l('http://drupal.org/project/advanced_help', 'http://drupal.org/project/advanced_help'),
'@hide' => url('admin/structure/views/settings'),
)));
}
}
}
/**
* Returns the results of the live preview.
*/
function views_ui_preview($view, $display_id, $args = array()) {
// When this function is invoked as a page callback, each Views argument is
// passed separately.
if (!is_array($args)) {
$args = array_slice(func_get_args(), 2);
}
// Save $_GET['q'] so it can be restored before returning from this function.
$q = $_GET['q'];
// Determine where the query and performance statistics should be output.
$show_query = variable_get('views_ui_show_sql_query', FALSE);
$show_info = variable_get('views_ui_show_preview_information', FALSE);
$show_location = variable_get('views_ui_show_sql_query_where', 'above');
$show_stats = variable_get('views_ui_show_performance_statistics', FALSE);
if ($show_stats) {
$show_stats = variable_get('views_ui_show_sql_query_where', 'above');
}
$combined = $show_query && $show_stats;
$rows = array('query' => array(), 'statistics' => array());
$output = '';
$errors = $view->validate();
if ($errors === TRUE) {
$view->ajax = TRUE;
$view->live_preview = TRUE;
$view->views_ui_context = TRUE;
// AJAX happens via $_POST but everything expects exposed data to be in
// GET. Copy stuff but remove ajax-framework specific keys. If we're
// clicking on links in a preview, though, we could actually still have
// some in $_GET, so we use $_REQUEST to ensure we get it all.
$exposed_input = $_REQUEST;
foreach (array('view_name', 'view_display_id', 'view_args', 'view_path', 'view_dom_id', 'pager_element', 'view_base_path', 'ajax_html_ids', 'ajax_page_state', 'form_id', 'form_build_id', 'form_token') as $key) {
if (isset($exposed_input[$key])) {
unset($exposed_input[$key]);
}
}
$view->set_exposed_input($exposed_input);
if (!$view->set_display($display_id)) {
return t('Invalid display id @display', array('@display' => $display_id));
}
$view->set_arguments($args);
// Store the current view URL for later use.
if ($view->display_handler->get_option('path')) {
$path = $view->get_url();
}
// Make view links come back to preview.
$view->override_path = 'admin/structure/views/nojs/preview/' . $view->name . '/' . $display_id;
// Also override $_GET['q'] so we get the pager.
$original_path = current_path();
$_GET['q'] = $view->override_path;
if ($args) {
$_GET['q'] .= '/' . implode('/', $args);
}
// Suppress contextual links of entities within the result set during a
// Preview.
// @todo We'll want to add contextual links specific to editing the View, so
// the suppression may need to be moved deeper into the Preview pipeline.
views_ui_contextual_links_suppress_push();
$preview = $view->preview($display_id, $args);
views_ui_contextual_links_suppress_pop();
// Reset variables.
unset($view->override_path);
$_GET['q'] = $original_path;
// Prepare the query information and statistics to show either above or
// below the view preview.
if ($show_info || $show_query || $show_stats) {
// Get information from the preview for display.
if (!empty($view->build_info['query'])) {
if ($show_query) {
$query = $view->build_info['query'];
// Only the SQL default class has a method getArguments.
$quoted = array();
if (get_class($view->query) == 'views_plugin_query_default') {
$quoted = $query->getArguments();
$connection = Database::getConnection();
foreach ($quoted as $key => $val) {
if (is_array($val)) {
$quoted[$key] = implode(', ', array_map(array($connection, 'quote'), $val));
}
else {
$quoted[$key] = $connection->quote($val);
}
}
}
$rows['query'][] = array('' . t('Query') . '', '
' . check_plain(strtr($query, $quoted)) . '
');
if (!empty($view->additional_queries)) {
$queries = '' . t('These queries were run during view rendering:') . '';
foreach ($view->additional_queries as $query) {
if ($queries) {
$queries .= "\n";
}
$queries .= t('[@time ms]', array('@time' => intval($query[1] * 100000) / 100)) . ' ' . $query[0];
}
$rows['query'][] = array('' . t('Other queries') . '', '
' . $queries . '
');
}
}
if ($show_info) {
$rows['query'][] = array('' . t('Title') . '', filter_xss_admin($view->get_title()));
if (isset($path)) {
$path = l($path, $path);
}
else {
$path = t('This display has no path.');
}
$rows['query'][] = array('' . t('Path') . '', $path);
}
if ($show_stats) {
$rows['statistics'][] = array('' . t('Query build time') . '', t('@time ms', array('@time' => intval($view->build_time * 100000) / 100)));
$rows['statistics'][] = array('' . t('Query execute time') . '', t('@time ms', array('@time' => intval($view->execute_time * 100000) / 100)));
$rows['statistics'][] = array('' . t('View render time') . '', t('@time ms', array('@time' => intval($view->render_time * 100000) / 100)));
}
drupal_alter('views_preview_info', $rows, $view);
}
else {
// No query was run. Display that information in place of either the
// query or the performance statistics, whichever comes first.
if ($combined || ($show_location === 'above')) {
$rows['query'] = array(array('' . t('Query') . '', t('No query was run')));
}
else {
$rows['statistics'] = array(array('' . t('Query') . '', t('No query was run')));
}
}
}
}
else {
foreach ($errors as $error) {
drupal_set_message($error, 'error');
}
$preview = t('Unable to preview due to validation errors.');
}
// Assemble the preview, the query info, and the query statistics in the
// requested order.
if ($show_location === 'above') {
if ($combined) {
$output .= '
';
}
$_GET['q'] = $q;
return $output;
}
/**
* Page callback to add a new view.
*/
function views_ui_add_page() {
views_ui_add_admin_css();
drupal_set_title(t('Add new view'));
return drupal_get_form('views_ui_add_form');
}
/**
* Form builder for the "add new view" page.
*/
function views_ui_add_form($form, &$form_state) {
ctools_include('dependent');
$form['#attached']['js'][] = drupal_get_path('module', 'views_ui') . '/js/views-admin.js';
$form['#attributes']['class'] = array('views-admin');
$form['human_name'] = array(
'#type' => 'textfield',
'#title' => t('View name'),
'#required' => TRUE,
'#size' => 32,
'#default_value' => !empty($form_state['view']) ? $form_state['view']->human_name : '',
'#maxlength' => 255,
);
$form['name'] = array(
'#type' => 'machine_name',
'#maxlength' => 128,
'#machine_name' => array(
'exists' => 'views_get_view',
'source' => array('human_name'),
),
'#description' => t('A unique machine-readable name for this View. It must only contain lowercase letters, numbers, and underscores.'),
);
$form['description_enable'] = array(
'#type' => 'checkbox',
'#title' => t('Description'),
);
$form['description'] = array(
'#type' => 'textfield',
'#title' => t('Provide description'),
'#title_display' => 'invisible',
'#size' => 64,
'#default_value' => !empty($form_state['view']) ? $form_state['view']->description : '',
'#dependency' => array(
'edit-description-enable' => array(1),
),
);
// Create a wrapper for the entire dynamic portion of the form. Everything
// that can be updated by AJAX goes somewhere inside here. For example, this
// is needed by "Show" dropdown (below); it changes the base table of the
// view and therefore potentially requires all options on the form to be
// dynamically updated.
$form['displays'] = array();
// Create the part of the form that allows the user to select the basic
// properties of what the view will display.
$form['displays']['show'] = array(
'#type' => 'fieldset',
'#tree' => TRUE,
'#attributes' => array('class' => array('container-inline')),
);
// Create the "Show" dropdown, which allows the base table of the view to be
// selected.
$wizard_plugins = views_ui_get_wizards();
$options = array();
foreach ($wizard_plugins as $key => $wizard) {
$options[$key] = $wizard['title'];
}
$form['displays']['show']['wizard_key'] = array(
'#type' => 'select',
'#title' => t('Show'),
'#options' => $options,
);
$show_form = &$form['displays']['show'];
$show_form['wizard_key']['#default_value'] = views_ui_get_selected($form_state, array('show', 'wizard_key'), 'node', $show_form['wizard_key']);
// Changing this dropdown updates the entire content of $form['displays'] via
// AJAX.
views_ui_add_ajax_trigger($show_form, 'wizard_key', array('displays'));
// Build the rest of the form based on the currently selected wizard plugin.
$wizard_key = $show_form['wizard_key']['#default_value'];
$get_instance = $wizard_plugins[$wizard_key]['get_instance'];
$wizard_instance = $get_instance($wizard_plugins[$wizard_key]);
$form = $wizard_instance->build_form($form, $form_state);
$form['save'] = array(
'#type' => 'submit',
'#value' => t('Save & exit'),
'#validate' => array('views_ui_wizard_form_validate'),
'#submit' => array('views_ui_add_form_save_submit'),
);
$form['continue'] = array(
'#type' => 'submit',
'#value' => t('Continue & edit'),
'#validate' => array('views_ui_wizard_form_validate'),
'#submit' => array('views_ui_add_form_store_edit_submit'),
'#process' => array_merge(array('views_ui_default_button'), element_info_property('submit', '#process', array())),
);
$form['cancel'] = array(
'#type' => 'submit',
'#value' => t('Cancel'),
'#submit' => array('views_ui_add_form_cancel_submit'),
'#limit_validation_errors' => array(),
);
return $form;
}
/**
* Helper form element validator: integer.
*
* The problem with this is that the function is private so it's not guaranteed
* that it might not be renamed/changed. In the future field.module or something
* else should provide a public validate function.
*
* @see _element_validate_integer_positive()
*/
function views_element_validate_integer($element, &$form_state) {
$value = $element['#value'];
if ($value !== '' && (!is_numeric($value) || intval($value) != $value || abs($value) != $value)) {
form_error($element, t('%name must be a positive integer.', array('%name' => $element['#title'])));
}
}
/**
* Gets the current value of a #select element, from within a form constructor.
*
* This function is intended for use in highly dynamic forms (in particular the
* add view wizard) which are rebuilt in different ways depending on which
* triggering element (AJAX or otherwise) was most recently fired. For example,
* sometimes it is necessary to decide how to build one dynamic form element
* based on the value of a different dynamic form element that may not have
* even been present on the form the last time it was submitted. This function
* takes care of resolving those conflicts and gives you the proper current
* value of the requested #select element.
*
* By necessity, this function sometimes uses non-validated user input from
* $form_state['input'] in making its determination. Although it performs some
* minor validation of its own, it is not complete. The intention is that the
* return value of this function should only be used to help decide how to
* build the current form the next time it is reloaded, not to be saved as if
* it had gone through the normal, final form validation process. Do NOT use
* the results of this function for any other purpose besides deciding how to
* build the next version of the form.
*
* @param array $form_state
* The standard associative array containing the current state of the form.
* @param array $parents
* An array of parent keys that point to the part of the submitted form
* values that are expected to contain the element's value (in the case where
* this form element was actually submitted). In a simple case (assuming
* #tree is TRUE throughout the form), if the select element is located in
* $form['wrapper']['select'], so that the submitted form values would
* normally be found in $form_state['values']['wrapper']['select'], you would
* pass array('wrapper', 'select') for this parameter.
* @param mixed $default_value
* The default value to return if the #select element does not currently have
* a proper value set based on the submitted input.
* @param array $element
* An array representing the current version of the #select element within
* the form.
*
* @return array
* The current value of the #select element. A common use for this is to feed
* it back into $element['#default_value'] so that the form will be rendered
* with the correct value selected.
*/
function views_ui_get_selected($form_state, $parents, $default_value, $element) {
// For now, don't trust this to work on anything but a #select element.
if (!isset($element['#type']) || $element['#type'] != 'select' || !isset($element['#options'])) {
return $default_value;
}
// If there is a user-submitted value for this element that matches one of
// the currently available options attached to it, use that. We need to check
// $form_state['input'] rather than $form_state['values'] here because the
// triggering element often has the #limit_validation_errors property set to
// prevent unwanted errors elsewhere on the form. This means that the
// $form_state['values'] array won't be complete. We could make it complete
// by adding each required part of the form to the #limit_validation_errors
// property individually as the form is being built, but this is difficult to
// do for a highly dynamic and extensible form. This method is much simpler.
if (!empty($form_state['input'])) {
$key_exists = NULL;
$submitted = drupal_array_get_nested_value($form_state['input'], $parents, $key_exists);
// Check that the user-submitted value is one of the allowed options before
// returning it. This is not a substitute for actual form validation;
// rather it is necessary because, for example, the same select element
// might have #options A, B, and C under one set of conditions but #options
// D, E, F under a different set of conditions. So the form submission
// might have occurred with option A selected, but when the form is rebuilt
// option A is no longer one of the choices. In that case, we don't want to
// use the value that was submitted anymore but rather fall back to the
// default value.
if ($key_exists && in_array($submitted, array_keys($element['#options']))) {
return $submitted;
}
}
// Fall back on returning the default value if nothing was returned above.
return $default_value;
}
/**
* Converts a form element in the add view wizard to be AJAX-enabled.
*
* This function takes a form element and adds AJAX behaviors to it such that
* changing it triggers another part of the form to update automatically. It
* also adds a submit button to the form that appears next to the triggering
* element and that duplicates its functionality for users who do not have
* JavaScript enabled (the button is automatically hidden for users who do have
* JavaScript).
*
* To use this function, call it directly from your form builder function
* immediately after you have defined the form element that will serve as the
* JavaScript trigger. Calling it elsewhere (such as in hook_form_alter()) may
* mean that the non-JavaScript fallback button does not appear in the correct
* place in the form.
*
* @param array $wrapping_element
* The element whose child will server as the AJAX trigger. For example, if
* $form['some_wrapper']['triggering_element'] represents the element which
* will trigger the AJAX behavior, you would pass $form['some_wrapper'] for
* this parameter.
* @param string $trigger_key
* The key within the wrapping element that identifies which of its children
* serves as the AJAX trigger. In the above example, you would pass
* 'triggering_element' for this parameter.
* @param array $refresh_parents
* An array of parent keys that point to the part of the form that will be
* refreshed by AJAX. For example, if triggering the AJAX behavior should
* cause $form['dynamic_content']['section'] to be refreshed, you would pass
* array('dynamic_content', 'section') for this parameter.
*/
function views_ui_add_ajax_trigger(&$wrapping_element, $trigger_key, $refresh_parents) {
$seen_ids = &drupal_static(__FUNCTION__ . ':seen_ids', array());
$seen_buttons = &drupal_static(__FUNCTION__ . ':seen_buttons', array());
// Add the AJAX behavior to the triggering element.
$triggering_element = &$wrapping_element[$trigger_key];
$triggering_element['#ajax']['callback'] = 'views_ui_ajax_update_form';
// We do not use drupal_html_id() to get an ID for the AJAX wrapper, because
// it remembers IDs across AJAX requests (and won't reuse them), but in our
// case we need to use the same ID from request to request so that the
// wrapper can be recognized by the AJAX system and its content can be
// dynamically updated. So instead, we will keep track of duplicate IDs
// (within a single request) on our own, later in this function.
$triggering_element['#ajax']['wrapper'] = 'edit-view-' . implode('-', $refresh_parents) . '-wrapper';
// Add a submit button for users who do not have JavaScript enabled. It
// should be displayed next to the triggering element on the form.
$button_key = $trigger_key . '_trigger_update';
$wrapping_element[$button_key] = array(
'#type' => 'submit',
// Hide this button when JavaScript is enabled.
'#attributes' => array('class' => array('js-hide')),
'#submit' => array('views_ui_nojs_submit'),
// Add a process function to limit this button's validation errors to the
// triggering element only. We have to do this in #process since until the
// form API has added the #parents property to the triggering element for
// us, we don't have any (easy) way to find out where its submitted values
// will eventually appear in $form_state['values'].
'#process' => array_merge(array('views_ui_add_limited_validation'), element_info_property('submit', '#process', array())),
// Add an after-build function that inserts a wrapper around the region of
// the form that needs to be refreshed by AJAX (so that the AJAX system can
// detect and dynamically update it). This is done in #after_build because
// it's a convenient place where we have automatic access to the complete
// form array, but also to minimize the chance that the HTML we add will
// get clobbered by code that runs after we have added it.
'#after_build' => array_merge(element_info_property('submit', '#after_build', array()), array('views_ui_add_ajax_wrapper')),
);
// Copy #weight and #access from the triggering element to the button, so
// that the two elements will be displayed together.
foreach (array('#weight', '#access') as $property) {
if (isset($triggering_element[$property])) {
$wrapping_element[$button_key][$property] = $triggering_element[$property];
}
}
// For easiest integration with the form API and the testing framework, we
// always give the button a unique #value, rather than playing around with
// #name.
$button_title = !empty($triggering_element['#title']) ? $triggering_element['#title'] : $trigger_key;
if (empty($seen_buttons[$button_title])) {
$wrapping_element[$button_key]['#value'] = t('Update "@title" choice', array(
'@title' => $button_title,
));
$seen_buttons[$button_title] = 1;
}
else {
$wrapping_element[$button_key]['#value'] = t('Update "@title" choice (@number)', array(
'@title' => $button_title,
'@number' => ++$seen_buttons[$button_title],
));
}
// Attach custom data to the triggering element and submit button, so we can
// use it in both the process function and AJAX callback.
$ajax_data = array(
'wrapper' => $triggering_element['#ajax']['wrapper'],
'trigger_key' => $trigger_key,
'refresh_parents' => $refresh_parents,
// Keep track of duplicate wrappers so we don't add the same wrapper to the
// page more than once.
'duplicate_wrapper' => !empty($seen_ids[$triggering_element['#ajax']['wrapper']]),
);
$seen_ids[$triggering_element['#ajax']['wrapper']] = TRUE;
$triggering_element['#views_ui_ajax_data'] = $ajax_data;
$wrapping_element[$button_key]['#views_ui_ajax_data'] = $ajax_data;
}
/**
* Processes a non-JS fallback submit button to limit its validation errors.
*/
function views_ui_add_limited_validation($element, &$form_state) {
// Retrieve the AJAX triggering element so we can determine its parents. (We
// know it's at the same level of the complete form array as the submit
// button, so all we have to do to find it is swap out the submit button's
// last array parent.)
$array_parents = $element['#array_parents'];
array_pop($array_parents);
$array_parents[] = $element['#views_ui_ajax_data']['trigger_key'];
$ajax_triggering_element = drupal_array_get_nested_value($form_state['complete form'], $array_parents);
// Limit this button's validation to the AJAX triggering element, so it can
// update the form for that change without requiring that the rest of the
// form be filled out properly yet.
$element['#limit_validation_errors'] = array($ajax_triggering_element['#parents']);
// If we are in the process of a form submission and this is the button that
// was clicked, the form API workflow in form_builder() will have already
// copied it to $form_state['triggering_element'] before our #process
// function is run. So we need to make the same modifications in $form_state
// as we did to the element itself, to ensure that #limit_validation_errors
// will actually be set in the correct place.
if (!empty($form_state['triggering_element'])) {
$clicked_button = &$form_state['triggering_element'];
if ($clicked_button['#name'] == $element['#name'] && $clicked_button['#value'] == $element['#value']) {
$clicked_button['#limit_validation_errors'] = $element['#limit_validation_errors'];
}
}
return $element;
}
/**
* After-build function that adds a wrapper to a form region (AJAX refreshes).
*
* This function inserts a wrapper around the region of the form that needs to
* be refreshed by AJAX, based on information stored in the corresponding submit
* button form element.
*/
function views_ui_add_ajax_wrapper($element, &$form_state) {
// Don't add the wrapper
if the same one was already inserted on this
// form.
if (empty($element['#views_ui_ajax_data']['duplicate_wrapper'])) {
// Find the region of the complete form that needs to be refreshed by AJAX.
// This was earlier stored in a property on the element.
$complete_form = &$form_state['complete form'];
$refresh_parents = $element['#views_ui_ajax_data']['refresh_parents'];
$refresh_element = drupal_array_get_nested_value($complete_form, $refresh_parents);
// The HTML ID that AJAX expects was also stored in a property on the
// element, so use that information to insert the wrapper
';
// Copy the element that needs to be refreshed back into the form, with our
// modifications to it.
drupal_array_set_nested_value($complete_form, $refresh_parents, $refresh_element);
}
return $element;
}
/**
* Updates a part of the add view form via AJAX.
*
* @return
* The part of the form that has changed.
*/
function views_ui_ajax_update_form($form, $form_state) {
// The region that needs to be updated was stored in a property of the
// triggering element by views_ui_add_ajax_trigger(), so all we have to do is
// retrieve that here.
return drupal_array_get_nested_value($form, $form_state['triggering_element']['#views_ui_ajax_data']['refresh_parents']);
}
/**
* Non-JavaScript fallback for updating the add view form.
*/
function views_ui_nojs_submit($form, &$form_state) {
$form_state['rebuild'] = TRUE;
}
/**
* Validate the add view form.
*/
function views_ui_wizard_form_validate($form, &$form_state) {
$wizard = views_ui_get_wizard($form_state['values']['show']['wizard_key']);
$form_state['wizard'] = $wizard;
$get_instance = $wizard['get_instance'];
$form_state['wizard_instance'] = $get_instance($wizard);
$errors = $form_state['wizard_instance']->validate($form, $form_state);
foreach ($errors as $name => $message) {
form_set_error($name, $message);
}
}
/**
* Process the add view form, 'save'.
*/
function views_ui_add_form_save_submit($form, &$form_state) {
try {
$view = $form_state['wizard_instance']->create_view($form, $form_state);
}
catch (ViewsWizardException $e) {
drupal_set_message($e->getMessage(), 'error');
$form_state['redirect'] = 'admin/structure/views';
}
$view->save();
$form_state['redirect'] = 'admin/structure/views';
if (!empty($view->display['page'])) {
$display = $view->display['page'];
if ($display->handler->has_path()) {
$one_path = $display->handler->get_option('path');
if (strpos($one_path, '%') === FALSE) {
$form_state['redirect'] = $one_path;
// PATH TO THE VIEW IF IT HAS ONE.
return;
}
}
}
drupal_set_message(t('Your view was saved. You may edit it from the list below.'));
}
/**
* Process the add view form, 'continue'.
*/
function views_ui_add_form_store_edit_submit($form, &$form_state) {
try {
$view = $form_state['wizard_instance']->create_view($form, $form_state);
}
catch (ViewsWizardException $e) {
drupal_set_message($e->getMessage(), 'error');
$form_state['redirect'] = 'admin/structure/views';
}
// Just cache it temporarily to edit it.
views_ui_cache_set($view);
// If there is a destination query, ensure we still redirect the user to the
// edit view page, and then redirect the user to the destination.
$destination = array();
if (isset($_GET['destination'])) {
$destination = drupal_get_destination();
unset($_GET['destination']);
}
$form_state['redirect'] = array('admin/structure/views/view/' . $view->name, array('query' => $destination));
}
/**
* Cancel the add view form.
*/
function views_ui_add_form_cancel_submit($form, &$form_state) {
$form_state['redirect'] = 'admin/structure/views';
}
/**
* Form element validation handler for a taxonomy autocomplete field.
*
* This allows a taxonomy autocomplete field to be validated outside the
* standard Field API workflow, without passing in a complete field widget.
* Instead, all that is required is that $element['#field_name'] contain the
* name of the taxonomy autocomplete field that is being validated.
*
* This function is currently not used for validation directly, although it
* could be. Instead, it is only used to store the term IDs and vocabulary name
* in the element value, based on the tags that the user typed in.
*
* @see taxonomy_autocomplete_validate()
*/
function views_ui_taxonomy_autocomplete_validate($element, &$form_state) {
$value = array();
if ($tags = $element['#value']) {
// Get the machine names of the vocabularies we will search, keyed by the
// vocabulary IDs.
$field = field_info_field($element['#field_name']);
$vocabularies = array();
if (!empty($field['settings']['allowed_values'])) {
foreach ($field['settings']['allowed_values'] as $tree) {
if ($vocabulary = taxonomy_vocabulary_machine_name_load($tree['vocabulary'])) {
$vocabularies[$vocabulary->vid] = $tree['vocabulary'];
}
}
}
// Store the term ID of each (valid) tag that the user typed.
$typed_terms = drupal_explode_tags($tags);
foreach ($typed_terms as $typed_term) {
if ($terms = taxonomy_term_load_multiple(array(), array('name' => trim($typed_term), 'vid' => array_keys($vocabularies)))) {
$term = array_pop($terms);
$value['tids'][] = $term->tid;
}
}
// Store the term IDs along with the name of the vocabulary. Currently
// Views (as well as the Field UI) assumes that there will only be one
// vocabulary, although technically the API allows there to be more than
// one.
if (!empty($value['tids'])) {
$value['tids'] = array_unique($value['tids']);
$value['vocabulary'] = array_pop($vocabularies);
}
}
form_set_value($element, $value, $form_state);
}
/**
* Theme function; returns basic administrative information about a view.
*
* TODO: template + preprocess.
*/
function theme_views_ui_view_info($variables) {
$view = $variables['view'];
$title = $view->get_human_name();
$displays = _views_ui_get_displays_list($view);
$displays = empty($displays) ? t('None') : format_plural(count($displays), 'Display', 'Displays') . ': ' . '' . implode(', ', $displays) . '';
switch ($view->type) {
case t('Default'):
default:
$type = t('In code');
break;
case t('Normal'):
$type = t('In database');
break;
case t('Overridden'):
$type = t('Database overriding code');
break;
}
$output = '';
$output .= '
\n";
return $output;
}
/**
* Page to delete a view.
*/
function views_ui_break_lock_confirm($form, &$form_state, $view) {
$form_state['view'] = &$view;
$form = array();
if (empty($view->locked)) {
$form['message']['#markup'] = t('There is no lock on view %name to break.', array('%name' => $view->name));
return $form;
}
$cancel = 'admin/structure/views/view/' . $view->name . '/edit';
$account = user_load($view->locked->uid);
return confirm_form($form,
t('Are you sure you want to break the lock on view %name?',
array('%name' => $view->name)),
$cancel,
t('By breaking this lock, any unsaved changes made by !user will be lost!', array('!user' => theme('username', array('account' => $account)))),
t('Break lock'),
t('Cancel'));
}
/**
* Submit handler to break_lock a view.
*/
function views_ui_break_lock_confirm_submit(&$form, &$form_state) {
ctools_object_cache_clear_all('view', $form_state['view']->name);
$form_state['redirect'] = 'admin/structure/views/view/' . $form_state['view']->name . '/edit';
drupal_set_message(t('The lock has been broken and you may now edit this view.'));
}
/**
* Helper function to return the used display_id for the edit page.
*
* This function handles access to the display.
*/
function views_ui_edit_page_display($view, $display_id) {
// Determine the displays available for editing.
if ($tabs = views_ui_edit_page_display_tabs($view, $display_id)) {
// If a display isn't specified, use the first one.
if (empty($display_id)) {
foreach ($tabs as $id => $tab) {
if (!isset($tab['#access']) || $tab['#access']) {
$display_id = $id;
break;
}
}
}
// If a display is specified, but we don't have access to it, return
// an access denied page.
if ($display_id && (!isset($tabs[$display_id]) || (isset($tabs[$display_id]['#access']) && !$tabs[$display_id]['#access']))) {
return MENU_ACCESS_DENIED;
}
return $display_id;
}
elseif ($display_id) {
return MENU_ACCESS_DENIED;
}
else {
$display_id = NULL;
}
return $display_id;
}
/**
* Page callback for the Edit View page.
*/
function views_ui_edit_page($view, $display_id = NULL) {
$display_id = views_ui_edit_page_display($view, $display_id);
if (!in_array($display_id, array(MENU_ACCESS_DENIED, MENU_NOT_FOUND))) {
$build = array();
$build['edit_form'] = drupal_get_form('views_ui_edit_form', $view, $display_id);
$build['preview'] = views_ui_build_preview($view, $display_id, FALSE);
}
else {
$build = $display_id;
}
return $build;
}
/**
*
*/
function views_ui_build_preview($view, $display_id, $render = TRUE) {
if (isset($_POST['ajax_html_ids'])) {
unset($_POST['ajax_html_ids']);
}
$build = array(
'#theme_wrappers' => array('container'),
'#attributes' => array('id' => 'views-preview-wrapper', 'class' => 'views-admin clearfix'),
);
$form_state = array('build_info' => array('args' => array($view, $display_id)));
$build['controls'] = drupal_build_form('views_ui_preview_form', $form_state);
$args = array();
if (!empty($form_state['values']['view_args'])) {
$args = explode('/', $form_state['values']['view_args']);
}
$build['preview'] = array(
'#theme_wrappers' => array('container'),
'#attributes' => array('id' => 'views-live-preview'),
'#markup' => $render ? views_ui_preview($view->clone_view(), $display_id, $args) : '',
);
return $build;
}
/**
* Form builder callback for editing a View.
*
* @todo Remove as many #prefix/#suffix lines as possible. Use #theme_wrappers
* instead.
*
* @todo Rename to views_ui_edit_view_form(). See that function for the "old"
* version.
*
* @see views_ui_ajax_get_form()
*/
function views_ui_edit_form($form, &$form_state, $view, $display_id = NULL) {
// Do not allow the form to be cached, because $form_state['view'] can become
// stale between page requests.
// See views_ui_ajax_get_form() for how this affects #ajax.
// @todo To remove this and allow the form to be cacheable:
// - Change $form_state['view'] to $form_state['temporary']['view'].
// - Add a #process function to initialize $form_state['temporary']['view']
// on cached form submissions.
// - Update ctools_include() to support cached forms, or else use
// form_load_include().
$form_state['no_cache'] = TRUE;
if ($display_id) {
if (!$view->set_display($display_id)) {
$form['#markup'] = t('Invalid display id @display', array('@display' => $display_id));
return $form;
}
$view->fix_missing_relationships();
}
ctools_include('dependent');
$form['#attached']['js'][] = ctools_attach_js('dependent');
$form['#attached']['js'][] = ctools_attach_js('collapsible-div');
$form['#tree'] = TRUE;
// @todo When more functionality is added to this form, cloning here may be
// too soon. But some of what we do with $view later in this function
// results in making it unserializable due to PDO limitations.
$form_state['view'] = clone $view;
$form['#attached']['library'][] = array('system', 'ui.tabs');
$form['#attached']['library'][] = array('system', 'ui.dialog');
$form['#attached']['library'][] = array('system', 'drupal.ajax');
$form['#attached']['library'][] = array('system', 'jquery.form');
// @todo This should be getting added to the page when an ajax popup calls
// for it, instead of having to add it manually here.
$form['#attached']['js'][] = 'misc/tabledrag.js';
$form['#attached']['css'] = views_ui_get_admin_css();
$module_path = drupal_get_path('module', 'views_ui');
$form['#attached']['js'][] = $module_path . '/js/views-admin.js';
$form['#attached']['js'][] = array(
'data' => array(
'views' => array(
'ajax' => array(
'id' => '#views-ajax-body',
'title' => '#views-ajax-title',
'popup' => '#views-ajax-popup',
'defaultForm' => views_ui_get_default_ajax_message(),
),
),
),
'type' => 'setting',
);
$form += array(
'#prefix' => '',
'#suffix' => '',
);
$form['#prefix'] = $form['#prefix'] . '
';
$form['#suffix'] = '
' . $form['#suffix'];
$form['#attributes']['class'] = array('form-edit');
if (isset($view->locked) && is_object($view->locked)) {
$form['locked'] = array(
'#theme_wrappers' => array('container'),
'#attributes' => array(
'class' => array('view-locked', 'messages', 'warning'),
),
'#markup' => t('This view is being edited by user !user, and is therefore locked from editing by others. This lock is !age old. Click here to break this lock.', array('!user' => theme('username', array('account' => user_load($view->locked->uid))), '!age' => format_interval(REQUEST_TIME - $view->locked->updated), '!break' => url('admin/structure/views/view/' . $view->name . '/break-lock'))),
);
}
if (isset($view->vid) && $view->vid == 'new') {
$message = t('* All changes are stored temporarily. Click Save to make your changes permanent. Click Cancel to discard the view.');
}
else {
$message = t('* All changes are stored temporarily. Click Save to make your changes permanent. Click Cancel to discard your changes.');
}
$form['changed'] = array(
'#theme_wrappers' => array('container'),
'#attributes' => array(
'class' => array('view-changed', 'messages', 'warning'),
),
'#markup' => $message,
);
if (empty($view->changed)) {
$form['changed']['#attributes']['class'][] = 'js-hide';
}
$form['help_text'] = array(
'#prefix' => '
',
'#suffix' => '
',
'#markup' => t('Modify the display(s) of your view below or add new displays.'),
);
$form['actions'] = array(
'#type' => 'actions',
'#weight' => 0,
);
if (empty($view->changed)) {
$form['actions']['#attributes'] = array(
'class' => array(
'js-hide',
),
);
}
$form['actions']['save'] = array(
'#type' => 'submit',
'#value' => t('Save'),
// Taken from the "old" UI. @todo: Review and rename.
'#validate' => array('views_ui_edit_view_form_validate'),
'#submit' => array('views_ui_edit_view_form_submit'),
);
$form['actions']['cancel'] = array(
'#type' => 'submit',
'#value' => t('Cancel'),
'#submit' => array('views_ui_edit_view_form_cancel'),
);
$form['displays'] = array(
'#prefix' => '
' . t('Displays') . "
\n"
. '
',
'#suffix' => '
',
);
$form['displays']['top'] = views_ui_render_display_top($view, $display_id);
// The rest requires a display to be selected.
if ($display_id) {
$form_state['display_id'] = $display_id;
// The part of the page where editing will take place. This element is the
// CTools collapsible-div container for the display edit elements.
$form['displays']['settings'] = array(
'#theme_wrappers' => array('container'),
'#attributes' => array(
'class' => array(
'views-display-settings',
'box-margin',
'ctools-collapsible-container',
),
),
'#id' => 'edit-display-settings',
);
$display_title = views_ui_get_display_label($view, $display_id, FALSE);
// Add a handle for the ctools collapsible-div. The handle is the title of
// the display.
$form['displays']['settings']['tab_title']['#markup'] = '
';
// Add a text that the display is disabled.
if (!empty($view->display[$display_id]->handler)) {
$enabled = $view->display[$display_id]->handler->get_option('enabled');
if (empty($enabled)) {
$form['displays']['settings']['disabled']['#markup'] = t('This display is disabled.');
}
}
// The ctools collapsible-div content.
$form['displays']['settings']['settings_content'] = array(
'#theme_wrappers' => array('container'),
'#id' => 'edit-display-settings-content',
'#attributes' => array(
'class' => array(
'ctools-collapsible-content',
),
),
);
// Add the edit display content.
$form['displays']['settings']['settings_content']['tab_content'] = views_ui_get_display_tab($view, $display_id);
$form['displays']['settings']['settings_content']['tab_content']['#theme_wrappers'] = array('container');
$form['displays']['settings']['settings_content']['tab_content']['#attributes'] = array('class' => array('views-display-tab'));
$form['displays']['settings']['settings_content']['tab_content']['#id'] = 'views-tab-' . $display_id;
// Mark deleted displays as such.
if (!empty($view->display[$display_id]->deleted)) {
$form['displays']['settings']['settings_content']['tab_content']['#attributes']['class'][] = 'views-display-deleted';
}
// Mark disabled displays as such.
if (empty($enabled)) {
$form['displays']['settings']['settings_content']['tab_content']['#attributes']['class'][] = 'views-display-disabled';
}
// The content of the popup dialog.
$form['ajax-area'] = array(
'#theme_wrappers' => array('container'),
'#id' => 'views-ajax-popup',
);
$form['ajax-area']['ajax-title'] = array(
'#markup' => '',
);
$form['ajax-area']['ajax-body'] = array(
'#theme_wrappers' => array('container'),
'#id' => 'views-ajax-body',
'#markup' => views_ui_get_default_ajax_message(),
);
}
// If relationships had to be fixed, we want to get that into the cache
// so that edits work properly, and to try to get the user to save it
// so that it's not using weird fixed up relationships.
if (!empty($view->relationships_changed) && empty($_POST)) {
drupal_set_message(t('This view has been automatically updated to fix missing relationships. While this View should continue to work, you should verify that the automatic updates are correct and save this view.'));
views_ui_cache_set($view);
}
return $form;
}
/**
* Provide the preview formulas and the preview output, too.
*/
function views_ui_preview_form($form, &$form_state, $view, $display_id = 'default') {
$form_state['no_cache'] = TRUE;
$form_state['view'] = $view;
$form['#attributes'] = array('class' => array('clearfix'));
// Add a checkbox controlling whether or not this display auto-previews.
$form['live_preview'] = array(
'#type' => 'checkbox',
'#id' => 'edit-displays-live-preview',
'#title' => t('Auto preview'),
'#default_value' => variable_get('views_ui_always_live_preview', TRUE),
);
// Add the arguments textfield.
$form['view_args'] = array(
'#type' => 'textfield',
'#title' => t('Preview with contextual filters:'),
'#description' => t('Separate contextual filter values with a "/". For example, %example.', array('%example' => '40/12/10')),
'#id' => 'preview-args',
// '#attributes' => array('class' => array('ctools-auto-submit')),
);
// Add the preview button.
$form['button'] = array(
'#type' => 'submit',
'#value' => t('Update preview'),
'#attributes' => array('class' => array('arguments-preview', 'ctools-auto-submit-click')),
'#pre_render' => array('ctools_dependent_pre_render'),
'#prefix' => '
',
'#suffix' => '
',
'#id' => 'preview-submit',
'#submit' => array('views_ui_edit_form_submit_preview'),
'#ajax' => array(
'path' => 'admin/structure/views/view/' . $view->name . '/preview/' . $display_id . '/ajax',
'wrapper' => 'views-preview-wrapper',
'event' => 'click',
'progress' => array('type' => 'throbber'),
'method' => 'replace',
),
// Make ENTER in arguments textfield (and other controls) submit the form
// as this button, not the Save button.
// @todo This only works for JS users. To make this work for nojs users,
// we may need to split Preview into a separate form.
'#process' => array_merge(array('views_ui_default_button'), element_info_property('submit', '#process', array())),
);
$form['#action'] = url('admin/structure/views/view/' . $view->name . '/preview/' . $display_id);
return $form;
}
/**
* Render the top of the display so it can be updated during ajax operations.
*/
function views_ui_render_display_top($view, $display_id) {
$element['#theme_wrappers'] = array('views_container');
$element['#attributes']['class'] = array('views-display-top', 'clearfix');
$element['#attributes']['id'] = array('views-display-top');
// Extra actions for the display.
$element['extra_actions'] = array(
'#theme' => 'links__ctools_dropbutton',
'#attributes' => array(
'id' => 'views-display-extra-actions',
'class' => array(
'horizontal', 'right', 'links', 'actions',
),
),
'#links' => array(
'edit-details' => array(
'title' => t('edit view name/description'),
'href' => "admin/structure/views/nojs/edit-details/$view->name",
'attributes' => array('class' => array('views-ajax-link')),
),
'analyze' => array(
'title' => t('analyze view'),
'href' => "admin/structure/views/nojs/analyze/$view->name/$display_id",
'attributes' => array('class' => array('views-ajax-link')),
),
'clone' => array(
'title' => t('clone view'),
'href' => "admin/structure/views/view/$view->name/clone",
),
'export' => array(
'title' => t('export view'),
'href' => "admin/structure/views/view/$view->name/export",
),
'reorder' => array(
'title' => t('reorder displays'),
'href' => "admin/structure/views/nojs/reorder-displays/$view->name/$display_id",
'attributes' => array('class' => array('views-ajax-link')),
),
),
);
// Let other modules add additional links here.
drupal_alter('views_ui_display_top_links', $element['extra_actions']['#links'], $view, $display_id);
if (isset($view->type) && $view->type != t('Default')) {
if ($view->type == t('Overridden')) {
$element['extra_actions']['#links']['revert'] = array(
'title' => t('revert view'),
'href' => "admin/structure/views/view/$view->name/revert",
'query' => array('destination' => "admin/structure/views/view/$view->name"),
);
}
else {
$element['extra_actions']['#links']['delete'] = array(
'title' => t('delete view'),
'href' => "admin/structure/views/view/$view->name/delete",
);
}
}
// Determine the displays available for editing.
if ($tabs = views_ui_edit_page_display_tabs($view, $display_id)) {
if ($display_id) {
$tabs[$display_id]['#active'] = TRUE;
}
$tabs['#prefix'] = '
' . t('Secondary tabs') . '
';
$tabs['#suffix'] = '
';
$element['tabs'] = $tabs;
}
// Buttons for adding a new display.
foreach (views_fetch_plugin_names('display', NULL, array($view->base_table)) as $type => $label) {
$element['add_display'][$type] = array(
'#type' => 'submit',
'#value' => t('Add !display', array('!display' => $label)),
'#limit_validation_errors' => array(),
'#submit' => array('views_ui_edit_form_submit_add_display', 'views_ui_edit_form_submit_delay_destination'),
'#attributes' => array('class' => array('add-display')),
// Allow JavaScript to remove the 'Add ' prefix from the button label when
// placing the button in a "Add" dropdown menu.
'#process' => array_merge(array('views_ui_form_button_was_clicked'), element_info_property('submit', '#process', array())),
'#values' => array(t('Add !display', array('!display' => $label)), $label),
);
}
return $element;
}
/**
*
*/
function views_ui_get_default_ajax_message() {
return '
' . t("Click on an item to edit that item's details.") . '
';
}
/**
* Submit handler to add a display to a view.
*/
function views_ui_edit_form_submit_add_display($form, &$form_state) {
$view = $form_state['view'];
// Create the new display.
$parents = $form_state['triggering_element']['#parents'];
$display_type = array_pop($parents);
$display_id = $view->add_display($display_type);
views_ui_cache_set($view);
// Redirect to the new display's edit page.
$form_state['redirect'] = 'admin/structure/views/view/' . $view->name . '/edit/' . $display_id;
}
/**
* Submit handler to duplicate a display for a view.
*/
function views_ui_edit_form_submit_duplicate_display($form, &$form_state) {
$view = $form_state['view'];
$display_id = $form_state['display_id'];
// Create the new display.
$display = $view->display[$display_id];
$new_display_id = $view->add_display($display->display_plugin);
$view->display[$new_display_id] = clone $display;
$view->display[$new_display_id]->id = $new_display_id;
// By setting the current display the changed marker will appear on the new
// display.
$view->current_display = $new_display_id;
views_ui_cache_set($view);
// Redirect to the new display's edit page.
$form_state['redirect'] = 'admin/structure/views/view/' . $view->name . '/edit/' . $new_display_id;
}
/**
* Submit handler to delete a display from a view.
*/
function views_ui_edit_form_submit_delete_display($form, &$form_state) {
$view = $form_state['view'];
$display_id = $form_state['display_id'];
// Mark the display for deletion.
$view->display[$display_id]->deleted = TRUE;
views_ui_cache_set($view);
// Redirect to the top-level edit page. The first remaining display will
// become the active display.
$form_state['redirect'] = 'admin/structure/views/view/' . $view->name;
}
/**
* Submit handler to add a restore a removed display to a view.
*/
function views_ui_edit_form_submit_undo_delete_display($form, &$form_state) {
// Create the new display.
$id = $form_state['display_id'];
$form_state['view']->display[$id]->deleted = FALSE;
// Store in cache.
views_ui_cache_set($form_state['view']);
// Redirect to the top-level edit page.
$form_state['redirect'] = 'admin/structure/views/view/' . $form_state['view']->name . '/edit/' . $id;
}
/**
* Submit handler to enable a disabled display.
*/
function views_ui_edit_form_submit_enable_display($form, &$form_state) {
$id = $form_state['display_id'];
// set_option doesn't work because this would might affect upper displays.
$form_state['view']->display[$id]->handler->set_option('enabled', TRUE);
// Store in cache.
views_ui_cache_set($form_state['view']);
// Redirect to the top-level edit page.
$form_state['redirect'] = 'admin/structure/views/view/' . $form_state['view']->name . '/edit/' . $id;
}
/**
* Submit handler to disable display.
*/
function views_ui_edit_form_submit_disable_display($form, &$form_state) {
$id = $form_state['display_id'];
$form_state['view']->display[$id]->handler->set_option('enabled', FALSE);
// Store in cache.
views_ui_cache_set($form_state['view']);
// Redirect to the top-level edit page.
$form_state['redirect'] = 'admin/structure/views/view/' . $form_state['view']->name . '/edit/' . $id;
}
/**
* Submit handler when Preview button is clicked.
*/
function views_ui_edit_form_submit_preview($form, &$form_state) {
// Rebuild the form with a pristine $view object.
$form_state['build_info']['args'][0] = views_ui_cache_load($form_state['view']->name);
$form_state['show_preview'] = TRUE;
$form_state['rebuild'] = TRUE;
}
/**
* Submit handler for form buttons that do not complete a form workflow.
*
* The Edit View form is a multistep form workflow, but with state managed by
* the CTools object cache rather than $form_state['rebuild']. Without this
* submit handler, buttons that add or remove displays would redirect to the
* destination parameter (e.g., when the Edit View form is linked to from a
* contextual link). This handler can be added to buttons whose form submission
* should not yet redirect to the destination.
*/
function views_ui_edit_form_submit_delay_destination($form, &$form_state) {
if (isset($_GET['destination']) && $form_state['redirect'] !== FALSE) {
if (!isset($form_state['redirect'])) {
$form_state['redirect'] = $_GET['q'];
}
if (is_string($form_state['redirect'])) {
$form_state['redirect'] = array($form_state['redirect']);
}
$options = isset($form_state['redirect'][1]) ? $form_state['redirect'][1] : array();
if (!isset($options['query']['destination'])) {
$options['query']['destination'] = $_GET['destination'];
}
$form_state['redirect'][1] = $options;
unset($_GET['destination']);
}
}
/**
* Adds tabs for navigating across Displays when editing a View.
*
* This function can be called from hook_menu_local_tasks_alter() to implement
* these tabs as secondary local tasks, or it can be called from elsewhere if
* having them as secondary local tasks isn't desired. The caller is responsible
* for setting the active tab's #active property to TRUE.
*
* @param view $view
* The view which will be edited.
* @param string $display_id
* The display_id which is edited on the current request.
*/
function views_ui_edit_page_display_tabs(view $view, $display_id = NULL) {
$tabs = array();
// Create a tab for each display.
foreach ($view->display as $id => $display) {
$tabs[$id] = array(
'#theme' => 'menu_local_task',
'#link' => array(
'title' => views_ui_get_display_label($view, $id),
'href' => 'admin/structure/views/view/' . $view->name . '/edit/' . $id,
'localized_options' => array(),
),
);
if (!empty($display->deleted)) {
$tabs[$id]['#link']['localized_options']['attributes']['class'][] = 'views-display-deleted-link';
}
if (isset($display->display_options['enabled']) && !$display->display_options['enabled']) {
$tabs[$id]['#link']['localized_options']['attributes']['class'][] = 'views-display-disabled-link';
}
}
// If the default display isn't supposed to be shown, don't display its tab,
// unless it's the only display.
if ((!views_ui_show_default_display($view) && $display_id != 'default') && count($tabs) > 1) {
$tabs['default']['#access'] = FALSE;
}
// Mark the display tab as red to show validation errors.
$view->validate();
foreach ($view->display as $id => $display) {
if (!empty($view->display_errors[$id])) {
// Always show the tab.
$tabs[$id]['#access'] = TRUE;
// Add a class to mark the error and a title to make a hover tip.
$tabs[$id]['#link']['localized_options']['attributes']['class'][] = 'error';
$tabs[$id]['#link']['localized_options']['attributes']['title'] = t('This display has one or more validation errors; please review it.');
}
}
return $tabs;
}
/**
* Controls whether or not the default display should have its own tab on edit.
*/
function views_ui_show_default_display($view) {
// Always show the default display for advanced users who prefer that mode.
$advanced_mode = variable_get('views_ui_show_master_display', FALSE);
// For other users, show the default display only if there are no others, and
// hide it if there's at least one "real" display.
$additional_displays = (count($view->display) == 1);
return $advanced_mode || $additional_displays;
}
/**
* Returns a renderable array representing the edit page for one display.
*/
function views_ui_get_display_tab($view, $display_id) {
$build = array();
$display = $view->display[$display_id];
// If the plugin doesn't exist, display an error message instead of an edit
// page.
if (empty($display->handler)) {
$title = isset($display->display_title) ? $display->display_title : t('Invalid');
// @todo: Improved UX for the case where a plugin is missing.
$build['#markup'] = t("Error: Display @display refers to a plugin named '@plugin', but that plugin is not available.", array('@display' => $display->id, '@plugin' => $display->display_plugin));
}
// Build the content of the edit page.
else {
$build['details'] = views_ui_get_display_tab_details($view, $display);
}
// In AJAX context, views_ui_regenerate_tab() returns this outside of form
// context, so hook_form_views_ui_edit_form_alter() is insufficient.
drupal_alter('views_ui_display_tab', $build, $view, $display_id);
return $build;
}
/**
* Helper function to get the display details section of the edit UI.
*
* @param view $view
* The full view object.
* @param object $display
* The display object to work with.
*
* @return array
* A renderable page build array.
*/
function views_ui_get_display_tab_details($view, $display) {
$display_title = views_ui_get_display_label($view, $display->id, FALSE);
$build = array(
'#theme_wrappers' => array('container'),
'#attributes' => array('id' => 'edit-display-settings-details'),
);
$plugin = views_fetch_plugin_data('display', $view->display[$display->id]->display_plugin);
// The following is for display purposes only. We need to determine if there
// is more than one button and wrap the buttons in a .ctools-dropbutton class
// if more than one is present. Otherwise, we'll just wrap the actions in the
// .ctools-button class.
$is_display_deleted = !empty($display->deleted);
$is_deletable = empty($plugin['no remove']);
// The master display cannot be cloned.
$is_default = $display->id == 'default';
// @todo: Figure out why get_option doesn't work here.
$is_enabled = $display->handler->get_option('enabled');
if (!$is_display_deleted && $is_deletable && !$is_default) {
$prefix = '
');
$build['columns']['third']['collapse']['#theme_wrappers'] = array('container');
$build['columns']['third']['collapse']['#attributes'] = array(
'class' => array('ctools-collapsible-content'),
);
// Each option (e.g. title, access, display as grid/table/list) fits into one
// of several "buckets," or boxes (Format, Fields, Sort, and so on).
$buckets = array();
// Fetch options from the display plugin, with a list of buckets they go into.
$options = array();
$display->handler->options_summary($buckets, $options);
// Place each option into its bucket.
foreach ($options as $id => $option) {
// Each option self-identifies as belonging in a particular bucket.
$buckets[$option['category']]['build'][$id] = views_ui_edit_form_get_build_from_option($id, $option, $view, $display);
}
// Place each bucket into the proper column.
foreach ($buckets as $id => $bucket) {
// Let buckets identify themselves as belonging in a column.
if (isset($bucket['column']) && isset($build['columns'][$bucket['column']])) {
$column = $bucket['column'];
}
// If a bucket doesn't pick one of our predefined columns to belong to, put
// it in the last one.
else {
$column = 'third';
}
if (isset($bucket['build']) && is_array($bucket['build'])) {
// The third column is a CTools collapsible div, so
// the structure of the form is a little different for this column.
if ($column === 'third') {
$build['columns'][$column]['collapse'][$id] = $bucket['build'];
$build['columns'][$column]['collapse'][$id]['#theme_wrappers'][] = 'views_ui_display_tab_bucket';
$build['columns'][$column]['collapse'][$id]['#title'] = !empty($bucket['title']) ? $bucket['title'] : '';
$build['columns'][$column]['collapse'][$id]['#name'] = !empty($bucket['title']) ? $bucket['title'] : $id;
}
else {
$build['columns'][$column][$id] = $bucket['build'];
$build['columns'][$column][$id]['#theme_wrappers'][] = 'views_ui_display_tab_bucket';
$build['columns'][$column][$id]['#title'] = !empty($bucket['title']) ? $bucket['title'] : '';
$build['columns'][$column][$id]['#name'] = !empty($bucket['title']) ? $bucket['title'] : $id;
}
}
}
$build['columns']['first']['fields'] = views_ui_edit_form_get_bucket('field', $view, $display);
$build['columns']['first']['filters'] = views_ui_edit_form_get_bucket('filter', $view, $display);
$build['columns']['first']['sorts'] = views_ui_edit_form_get_bucket('sort', $view, $display);
$build['columns']['second']['header'] = views_ui_edit_form_get_bucket('header', $view, $display);
$build['columns']['second']['footer'] = views_ui_edit_form_get_bucket('footer', $view, $display);
$build['columns']['third']['collapse']['arguments'] = views_ui_edit_form_get_bucket('argument', $view, $display);
$build['columns']['third']['collapse']['relationships'] = views_ui_edit_form_get_bucket('relationship', $view, $display);
$build['columns']['third']['collapse']['empty'] = views_ui_edit_form_get_bucket('empty', $view, $display);
return $build;
}
/**
* Build a renderable array representing one option on the edit form.
*
* This function might be more logical as a method on an object, if a suitable
* object emerges out of refactoring.
*/
function views_ui_edit_form_get_build_from_option($id, $option, $view, $display) {
$option_build = array();
$option_build['#theme'] = 'views_ui_display_tab_setting';
$option_build['#description'] = $option['title'];
$option_build['#link'] = $display->handler->option_link($option['value'], $id, '', empty($option['desc']) ? '' : $option['desc']);
$option_build['#links'] = array();
if (!empty($option['links']) && is_array($option['links'])) {
foreach ($option['links'] as $link_id => $link_value) {
$option_build['#settings_links'][] = $display->handler->option_link($option['setting'], $link_id, 'views-button-configure', $link_value);
}
}
if (!empty($display->handler->options['defaults'][$id])) {
$display_id = 'default';
$option_build['#defaulted'] = TRUE;
}
else {
$display_id = $display->id;
if (!$display->handler->is_default_display()) {
if ($display->handler->defaultable_sections($id)) {
$option_build['#overridden'] = TRUE;
}
}
}
$option_build['#attributes']['class'][] = drupal_clean_css_identifier($display_id . '-' . $id);
if (!empty($view->changed_sections[$display_id . '-' . $id])) {
$option_build['#changed'] = TRUE;
}
return $option_build;
}
/**
*
*/
function template_preprocess_views_ui_display_tab_setting(&$variables) {
static $zebra = 0;
$variables['zebra'] = ($zebra % 2 === 0 ? 'odd' : 'even');
$zebra++;
// Put the main link to the left side.
array_unshift($variables['settings_links'], $variables['link']);
$variables['settings_links'] = implode(' | ', $variables['settings_links']);
// Add classes associated with this display tab to the overall list.
$variables['classes_array'] = array_merge($variables['classes_array'], $variables['class']);
if (!empty($variables['defaulted'])) {
$variables['classes_array'][] = 'defaulted';
}
if (!empty($variables['overridden'])) {
$variables['classes_array'][] = 'overridden';
$variables['attributes_array']['title'][] = t('Overridden');
}
// Append a colon to the description, if requested.
if ($variables['description'] && $variables['description_separator']) {
$variables['description'] .= t(':');
}
}
/**
*
*/
function template_preprocess_views_ui_display_tab_bucket(&$variables) {
$element = $variables['element'];
$variables['item_help_icon'] = '';
if (!empty($element['#item_help_icon'])) {
$variables['item_help_icon'] = render($element['#item_help_icon']);
}
if (!empty($element['#name'])) {
$variables['classes_array'][] = drupal_clean_css_identifier(strtolower($element['#name']));
}
if (!empty($element['#overridden'])) {
$variables['classes_array'][] = 'overridden';
$variables['attributes_array']['title'][] = t('Overridden');
}
$variables['content'] = $element['#children'];
$variables['title'] = $element['#title'];
$variables['actions'] = !empty($element['#actions']) ? $element['#actions'] : '';
}
/**
*
*/
function template_preprocess_views_ui_display_tab_column(&$variables) {
$element = $variables['element'];
$variables['content'] = $element['#children'];
$variables['column'] = $element['#column'];
}
/**
* Move form elements into fieldsets for presentation purposes.
*
* Many views forms use #tree = TRUE to keep their values in a hierarchy for
* easier storage. Moving the form elements into fieldsets during form building
* would break up that hierarchy. Therefore, we wait until the pre_render stage,
* where any changes we make affect presentation only and aren't reflected in
* $form_state['values'].
*/
function views_ui_pre_render_add_fieldset_markup($form) {
foreach (element_children($form) as $key) {
$element = $form[$key];
// In our form builder functions, we added an arbitrary #fieldset property
// to any element that belongs in a fieldset. If this form element has that
// property, move it into its fieldset.
if (isset($element['#fieldset']) && isset($form[$element['#fieldset']])) {
$form[$element['#fieldset']][$key] = $element;
// Remove the original element this duplicates.
unset($form[$key]);
}
}
return $form;
}
/**
* Flattens the structure of an element containing the #flatten property.
*
* If a form element has #flatten = TRUE, then all of it's children
* get moved to the same level as the element itself.
* So $form['to_be_flattened'][$key] becomes $form[$key], and
* $form['to_be_flattened'] gets unset.
*/
function views_ui_pre_render_flatten_data($form) {
foreach (element_children($form) as $key) {
$element = $form[$key];
if (!empty($element['#flatten'])) {
foreach (element_children($element) as $child_key) {
$form[$child_key] = $form[$key][$child_key];
}
// All done, remove the now-empty parent.
unset($form[$key]);
}
}
return $form;
}
/**
* Moves argument options into their place.
*
* When configuring the default argument behavior, almost each of the radio
* buttons has its own fieldset shown bellow it when the radio button is
* clicked. That fieldset is created through a custom form process callback.
* Each element that has #argument_option defined and pointing to a default
* behavior gets moved to the appropriate fieldset.
* So if #argument_option is specified as 'default', the element is moved
* to the 'default_options' fieldset.
*/
function views_ui_pre_render_move_argument_options($form) {
foreach (element_children($form) as $key) {
$element = $form[$key];
if (!empty($element['#argument_option'])) {
$container_name = $element['#argument_option'] . '_options';
if (isset($form['no_argument']['default_action'][$container_name])) {
$form['no_argument']['default_action'][$container_name][$key] = $element;
}
// Remove the original element this duplicates.
unset($form[$key]);
}
}
return $form;
}
/**
* Custom form radios process function.
*
* Roll out a single radios element to a list of radios,
* using the options array as index.
* While doing that, create a container element underneath each option, which
* contains the settings related to that option.
*
* @see form_process_radios()
*/
function views_ui_process_container_radios($element) {
if (count($element['#options']) > 0) {
foreach ($element['#options'] as $key => $choice) {
$element += array($key => array());
// Generate the parents as the autogenerator does, so we will have a
// unique id for each radio button.
$parents_for_id = array_merge($element['#parents'], array($key));
$element[$key] += array(
'#type' => 'radio',
'#title' => $choice,
// The key is sanitized in drupal_attributes() during output from the
// theme function.
'#return_value' => $key,
'#default_value' => isset($element['#default_value']) ? $element['#default_value'] : NULL,
'#attributes' => $element['#attributes'],
'#parents' => $element['#parents'],
'#id' => drupal_html_id('edit-' . implode('-', $parents_for_id)),
'#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL,
);
$element[$key . '_options'] = array(
'#type' => 'container',
'#attributes' => array('class' => array('views-admin-dependent')),
);
}
}
return $element;
}
/**
* Import a view from cut & paste.
*/
function views_ui_import_page($form, &$form_state) {
$form['name'] = array(
'#type' => 'textfield',
'#title' => t('View name'),
'#description' => t('Enter the name to use for this view if it is different from the source view. Leave blank to use the name of the view.'),
);
$form['name_override'] = array(
'#type' => 'checkbox',
'#title' => t('Replace an existing view if one exists with the same name'),
);
$form['bypass_validation'] = array(
'#type' => 'checkbox',
'#title' => t('Bypass view validation'),
'#description' => t('Bypass the validation of plugins and handlers when importing this view.'),
);
$form['view'] = array(
'#type' => 'textarea',
'#title' => t('Paste view code here'),
'#required' => TRUE,
);
$form['submit'] = array(
'#type' => 'submit',
'#value' => t('Import'),
'#submit' => array('views_ui_import_submit'),
'#validate' => array('views_ui_import_validate'),
);
return $form;
}
/**
* Validate handler to import a view.
*/
function views_ui_import_validate($form, &$form_state) {
$view = '';
views_include('view');
// Be forgiving if someone pastes views code that starts with 'api_version) || $view->api_version < 2) {
form_error($form['view'], t('That view is not compatible with this version of Views.
If you have a view from views1 you have to go to a drupal6 installation and import it there.'));
}
elseif (version_compare($view->api_version, views_api_version(), '>')) {
form_error($form['view'], t('That view is created for the version @import_version of views, but you only have @api_version', array(
'@import_version' => $view->api_version,
'@api_version' => views_api_version(),
)));
}
// View name must be alphanumeric or underscores, no other punctuation.
if (!empty($form_state['values']['name']) && preg_match('/[^a-zA-Z0-9_]/', $form_state['values']['name'])) {
form_error($form['name'], t('View name must be alphanumeric or underscores only.'));
}
if ($form_state['values']['name']) {
$view->name = $form_state['values']['name'];
}
$test = views_get_view($view->name);
if (!$form_state['values']['name_override']) {
if ($test && $test->type != t('Default')) {
form_set_error('', t('A view by that name already exists; please choose a different name'));
}
}
else {
if ($test->vid) {
$view->vid = $test->vid;
}
}
// Make sure base table gets set properly if it got moved.
$view->update();
$view->init_display();
$broken = FALSE;
// Bypass the validation of view pluigns/handlers if option is checked.
if (!$form_state['values']['bypass_validation']) {
// Make sure that all plugins and handlers needed by this view actually
// exist.
foreach ($view->display as $id => $display) {
if (empty($display->handler) || !empty($display->handler->broken)) {
drupal_set_message(t('Display plugin @plugin is not available.', array(
'@plugin' => $display->display_plugin,
)), 'error');
$broken = TRUE;
continue;
}
$plugin = views_get_plugin('style', $display->handler->get_option('style_plugin'));
if (!$plugin) {
drupal_set_message(t('Style plugin @plugin is not available.', array(
'@plugin' => $display->handler->get_option('style_plugin'),
)), 'error');
$broken = TRUE;
}
elseif ($plugin->uses_row_plugin()) {
$plugin = views_get_plugin('row', $display->handler->get_option('row_plugin'));
if (!$plugin) {
drupal_set_message(t('Row plugin @plugin is not available.', array(
'@plugin' => $display->handler->get_option('row_plugin'),
)), 'error');
$broken = TRUE;
}
}
foreach (views_object_types() as $type => $info) {
$handlers = $display->handler->get_handlers($type);
if ($handlers) {
foreach ($handlers as $id => $handler) {
if ($handler->broken()) {
drupal_set_message(t('@type handler @table.@field is not available.', array(
'@type' => $info['stitle'],
'@table' => $handler->table,
'@field' => $handler->field,
)), 'error');
$broken = TRUE;
}
}
}
}
}
}
if ($broken) {
form_set_error('', t('Unable to import view.'));
}
$form_state['view'] = &$view;
}
/**
* Submit handler for view import.
*/
function views_ui_import_submit($form, &$form_state) {
// Store in cache and then go to edit.
views_ui_cache_set($form_state['view']);
$form_state['redirect'] = 'admin/structure/views/view/' . $form_state['view']->name . '/edit';
}
/**
* Validate that a view is complete and whole.
*/
function views_ui_edit_view_form_validate($form, &$form_state) {
// Do not validate cancel or delete or revert.
if (empty($form_state['clicked_button']['#value']) || $form_state['clicked_button']['#value'] != t('Save')) {
return;
}
$errors = $form_state['view']->validate();
if ($errors !== TRUE) {
foreach ($errors as $error) {
form_set_error('', $error);
}
}
}
/**
* Submit handler for the edit view form.
*/
function views_ui_edit_view_form_submit($form, &$form_state) {
// Go through and remove displayed scheduled for removal.
foreach ($form_state['view']->display as $id => $display) {
if (!empty($display->deleted)) {
unset($form_state['view']->display[$id]);
}
}
// Rename display ids if needed.
foreach ($form_state['view']->display as $id => $display) {
if (!empty($display->new_id)) {
$form_state['view']->display[$id]->id = $display->new_id;
// Redirect the user to the renamed display to be sure that the page
// itself exists and doesn't throw errors.
$form_state['redirect'] = 'admin/structure/views/view/' . $form_state['view']->name . '/edit/' . $display->new_id;
}
}
// Direct the user to the right url, if the path of the display has changed.
if (!empty($_GET['destination'])) {
$destination = $_GET['destination'];
// Find out the first display which has a changed path and redirect to this
// URL.
$old_view = views_get_view($form_state['view']->name);
foreach ($old_view->display as $id => $display) {
// Only check for displays with a path.
if (!isset($display->display_options['path'])) {
continue;
}
$old_path = $display->display_options['path'];
if (($display->display_plugin == 'page') && ($old_path == $destination) && ($old_path != $form_state['view']->display[$id]->display_options['path'])) {
$destination = $form_state['view']->display[$id]->display_options['path'];
unset($_GET['destination']);
}
}
$form_state['redirect'] = $destination;
}
$form_state['view']->save();
drupal_set_message(t('The view %name has been saved.', array('%name' => $form_state['view']->get_human_name())));
// Remove this view from cache so we can edit it properly.
ctools_object_cache_clear('view', $form_state['view']->name);
}
/**
* Submit handler for the edit view form.
*/
function views_ui_edit_view_form_cancel($form, &$form_state) {
// Remove this view from cache so edits will be lost.
ctools_object_cache_clear('view', $form_state['view']->name);
if (empty($form['view']->vid)) {
// I seem to have to drupal_goto here because I can't get fapi to
// honor the redirect target. Not sure what I screwed up here.
drupal_goto('admin/structure/views');
}
}
/**
*
*/
function views_ui_edit_view_form_delete($form, &$form_state) {
unset($_REQUEST['destination']);
// Redirect to the delete confirm page.
$form_state['redirect'] = array('admin/structure/views/view/' . $form_state['view']->name . '/delete', array('query' => drupal_get_destination() + array('cancel' => 'admin/structure/views/view/' . $form_state['view']->name . '/edit')));
}
/**
* Add information about a section to a display.
*/
function views_ui_edit_form_get_bucket($type, $view, $display) {
$build = array(
'#theme_wrappers' => array('views_ui_display_tab_bucket'),
);
$types = views_object_types();
$build['#overridden'] = FALSE;
$build['#defaulted'] = FALSE;
if (module_exists('advanced_help')) {
$build['#item_help_icon'] = array(
'#theme' => 'advanced_help_topic',
'#module' => 'views',
'#topic' => $type,
);
}
$build['#name'] = $build['#title'] = $types[$type]['title'];
// Different types now have different rearrange forms, so we use this switch
// to get the right one.
switch ($type) {
case 'filter':
$rearrange_url = "admin/structure/views/nojs/rearrange-$type/$view->name/$display->id/$type";
$rearrange_text = t('And/Or, Rearrange');
// @todo Add another class to have another symbol for filter rearrange.
$class = 'icon compact rearrange';
break;
case 'field':
// Fetch the style plugin info so we know whether to list fields or not.
$style_plugin = $display->handler->get_plugin();
$uses_fields = $style_plugin && $style_plugin->uses_fields();
if (!$uses_fields) {
$build['fields'][] = array(
'#markup' => t('The selected style or row format does not utilize fields.'),
'#theme_wrappers' => array('views_container'),
'#attributes' => array('class' => array('views-display-setting')),
);
return $build;
}
default:
$rearrange_url = "admin/structure/views/nojs/rearrange/$view->name/$display->id/$type";
$rearrange_text = t('Rearrange');
$class = 'icon compact rearrange';
}
// Create an array of actions to pass to theme_links
$actions = array();
$count_handlers = count($display->handler->get_handlers($type));
$actions['add'] = array(
'title' => t('Add'),
'href' => "admin/structure/views/nojs/add-item/$view->name/$display->id/$type",
'attributes' => array('class' => array('icon compact add', 'views-ajax-link'), 'title' => t('Add'), 'id' => 'views-add-' . $type),
'html' => TRUE,
);
if ($count_handlers > 0) {
$actions['rearrange'] = array(
'title' => $rearrange_text,
'href' => $rearrange_url,
'attributes' => array('class' => array($class, 'views-ajax-link'), 'title' => t('Rearrange'), 'id' => 'views-rearrange-' . $type),
'html' => TRUE,
);
}
// Render the array of links.
$build['#actions'] = theme('links__ctools_dropbutton',
array(
'links' => $actions,
'attributes' => array(
'class' => array('inline', 'links', 'actions', 'horizontal', 'right'),
),
'class' => array('views-ui-settings-bucket-operations'),
)
);
if (!$display->handler->is_default_display()) {
if (!$display->handler->is_defaulted($types[$type]['plural'])) {
$build['#overridden'] = TRUE;
}
else {
$build['#defaulted'] = TRUE;
}
}
// If there's an options form for the bucket, link to it.
if (!empty($types[$type]['options'])) {
$build['#title'] = l($build['#title'], "admin/structure/views/nojs/config-type/$view->name/$display->id/$type", array('attributes' => array('class' => array('views-ajax-link'), 'id' => 'views-title-' . $type)));
}
static $relationships = NULL;
if (!isset($relationships)) {
// Get relationship labels.
$relationships = array();
// @todo: get_handlers()
$handlers = $display->handler->get_option('relationships');
if ($handlers) {
foreach ($handlers as $id => $info) {
$handler = $display->handler->get_handler('relationship', $id);
$relationships[$id] = $handler->label();
}
}
}
// Filters can now be grouped so we do a little bit extra.
$groups = array();
$grouping = FALSE;
if ($type == 'filter') {
$group_info = $view->display_handler->get_option('filter_groups');
// If there is only one group but it is using the "OR" filter, we still
// treat it as a group for display purposes, since we want to display the
// "OR" label next to items within the group.
if (!empty($group_info['groups']) && (count($group_info['groups']) > 1 || current($group_info['groups']) == 'OR')) {
$grouping = TRUE;
$groups = array(0 => array());
}
}
$build['fields'] = array();
foreach ($display->handler->get_option($types[$type]['plural']) as $id => $field) {
// Build the option link for this handler ("Node: ID = article").
$build['fields'][$id] = array();
$build['fields'][$id]['#theme'] = 'views_ui_display_tab_setting';
$handler = $display->handler->get_handler($type, $id);
if (empty($handler)) {
$build['fields'][$id]['#class'][] = 'broken';
$field_name = t('Broken/missing handler: @table > @field', array('@table' => $field['table'], '@field' => $field['field']));
$build['fields'][$id]['#link'] = l($field_name, "admin/structure/views/nojs/config-item/$view->name/$display->id/$type/$id", array('attributes' => array('class' => array('views-ajax-link')), 'html' => TRUE));
continue;
}
$field_name = check_plain($handler->ui_name(TRUE));
if (!empty($field['relationship']) && !empty($relationships[$field['relationship']])) {
$field_name = '(' . $relationships[$field['relationship']] . ') ' . $field_name;
}
$description = filter_xss_admin($handler->admin_summary());
$link_text = $field_name . (empty($description) ? '' : " ($description)");
$link_attributes = array('class' => array('views-ajax-link'));
if (!empty($field['exclude'])) {
$link_attributes['class'][] = 'views-field-excluded';
}
$build['fields'][$id]['#link'] = l($link_text, "admin/structure/views/nojs/config-item/$view->name/$display->id/$type/$id", array('attributes' => $link_attributes, 'html' => TRUE));
$build['fields'][$id]['#class'][] = drupal_clean_css_identifier($display->id . '-' . $type . '-' . $id);
if (!empty($view->changed_sections[$display->id . '-' . $type . '-' . $id])) {
// @todo: #changed is no longer being used?
$build['fields'][$id]['#changed'] = TRUE;
}
if ($display->handler->use_group_by() && $handler->use_group_by()) {
$build['fields'][$id]['#settings_links'][] = l('' . t('Aggregation settings') . '', "admin/structure/views/nojs/config-item-group/$view->name/$display->id/$type/$id", array('attributes' => array('class' => 'views-button-configure views-ajax-link', 'title' => t('Aggregation settings')), 'html' => TRUE));
}
if ($handler->has_extra_options()) {
$build['fields'][$id]['#settings_links'][] = l('' . t('Settings') . '', "admin/structure/views/nojs/config-item-extra/$view->name/$display->id/$type/$id", array('attributes' => array('class' => array('views-button-configure', 'views-ajax-link'), 'title' => t('Settings')), 'html' => TRUE));
}
if ($grouping) {
$gid = $handler->options['group'];
// Show in default group if the group does not exist.
if (empty($group_info['groups'][$gid])) {
$gid = 0;
}
$groups[$gid][] = $id;
}
}
// If using grouping, re-order fields so that they show up properly in the
// list.
if ($type == 'filter' && $grouping) {
$store = $build['fields'];
$build['fields'] = array();
foreach ($groups as $gid => $contents) {
// Display an operator between each group.
if (!empty($build['fields'])) {
$build['fields'][] = array(
'#theme' => 'views_ui_display_tab_setting',
'#class' => array('views-group-text'),
'#link' => ($group_info['operator'] == 'OR' ? t('OR') : t('AND')),
);
}
// Display an operator between each pair of filters within the group.
$keys = array_keys($contents);
$last = end($keys);
foreach ($contents as $key => $pid) {
if ($key != $last) {
$store[$pid]['#link'] .= ' ' . ($group_info['groups'][$gid] == 'OR' ? t('OR') : t('AND'));
}
$build['fields'][$pid] = $store[$pid];
}
}
}
return $build;
}
/**
* Regenerate the current tab for AJAX updates.
*/
function views_ui_regenerate_tab(&$view, &$output, $display_id) {
if (!$view->set_display('default')) {
return;
}
// Regenerate the main display area.
$build = views_ui_get_display_tab($view, $display_id);
views_ui_add_microweights($build);
$output[] = ajax_command_html('#views-tab-' . $display_id, drupal_render($build));
// Regenerate the top area so changes to display names and order will appear.
$build = views_ui_render_display_top($view, $display_id);
views_ui_add_microweights($build);
$output[] = ajax_command_replace('#views-display-top', drupal_render($build));
}
/**
* Recursively adds microweights to a render array.
*
* Similar to what form_builder() does for forms.
*
* @todo Submit a core patch to fix drupal_render() to do this, so that all
* render arrays automatically preserve array insertion order, as forms do.
*/
function views_ui_add_microweights(&$build) {
$count = 0;
foreach (element_children($build) as $key) {
if (!isset($build[$key]['#weight'])) {
$build[$key]['#weight'] = $count / 1000;
}
views_ui_add_microweights($build[$key]);
$count++;
}
}
/**
* Provide a standard set of Apply/Cancel/OK buttons for the forms. Also provide
* a hidden op operator because the forms plugin doesn't seem to properly
* provide which button was clicked.
*
* TODO: Is the hidden op operator still here somewhere, or is that part of the
* docblock outdated?
*/
function views_ui_standard_form_buttons(&$form, &$form_state, $form_id, $name = NULL, $third = NULL, $submit = NULL) {
$form['buttons'] = array(
'#prefix' => '
',
'#suffix' => '
',
);
if (empty($name)) {
$name = t('Apply');
$view = $form_state['view'];
if (!empty($view->stack) && count($view->stack) > 1) {
$name = t('Apply and continue');
}
$names = array(t('Apply'), t('Apply and continue'));
}
// Forms that are purely informational set an ok_button flag, so we know not
// to create an "Apply" button for them.
if (empty($form_state['ok_button'])) {
$form['buttons']['submit'] = array(
'#type' => 'submit',
'#value' => $name,
// The regular submit handler ($form_id . '_submit') does not apply if
// we're updating the default display. It does apply if we're updating
// the current display. Since we have no way of knowing at this point
// which display the user wants to update, views_ui_standard_submit will
// take care of running the regular submit handler as appropriate.
'#submit' => array('views_ui_standard_submit'),
);
// Form API button click detection requires the button's #value to be the
// same between the form build of the initial page request, and the initial
// form build of the request processing the form submission. Ideally, the
// button's #value shouldn't change until the form rebuild step. However,
// views_ui_ajax_form() implements a different multistep form workflow than
// the Form API does, and adjusts $view->stack prior to form processing, so
// we compensate by extending button click detection code to support any of
// the possible button labels.
if (isset($names)) {
$form['buttons']['submit']['#values'] = $names;
$form['buttons']['submit']['#process'] = array_merge(array('views_ui_form_button_was_clicked'), element_info_property($form['buttons']['submit']['#type'], '#process', array()));
}
// If a validation handler exists for the form, assign it to this button.
if (function_exists($form_id . '_validate')) {
$form['buttons']['submit']['#validate'][] = $form_id . '_validate';
}
}
// Create a "Cancel" button. For purely informational forms, label it "OK".
$cancel_submit = function_exists($form_id . '_cancel') ? $form_id . '_cancel' : 'views_ui_standard_cancel';
$form['buttons']['cancel'] = array(
'#type' => 'submit',
'#value' => empty($form_state['ok_button']) ? t('Cancel') : t('Ok'),
'#submit' => array($cancel_submit),
'#validate' => array(),
);
// Some forms specify a third button, with a name and submit handler.
if ($third) {
if (empty($submit)) {
$submit = 'third';
}
$third_submit = function_exists($form_id . '_' . $submit) ? $form_id . '_' . $submit : 'views_ui_standard_cancel';
$form['buttons'][$submit] = array(
'#type' => 'submit',
'#value' => $third,
'#validate' => array(),
'#submit' => array($third_submit),
);
}
// Compatibility, to be removed later: // @todo When is "later"? We used to
// set these items on the form, but now we want them on the $form_state.
if (isset($form['#title'])) {
$form_state['title'] = $form['#title'];
}
if (isset($form['#help_topic'])) {
$form_state['help_topic'] = $form['#help_topic'];
}
if (isset($form['#help_module'])) {
$form_state['help_module'] = $form['#help_module'];
}
if (isset($form['#url'])) {
$form_state['url'] = $form['#url'];
}
if (isset($form['#section'])) {
$form_state['#section'] = $form['#section'];
}
// Finally, we never want these cached -- our object cache does that for us.
$form['#no_cache'] = TRUE;
// If this isn't an ajaxy form, then we want to set the title.
if (!empty($form['#title'])) {
drupal_set_title($form['#title']);
}
}
/**
* Basic submit handler applicable to all 'standard' forms.
*
* This submit handler determines whether the user wants the submitted changes
* to apply to the default display or to the current display, and dispatches
* control appropriately.
*/
function views_ui_standard_submit($form, &$form_state) {
// Determine whether the values the user entered are intended to apply to
// the current display or the default display.
list($was_defaulted, $is_defaulted, $revert) = views_ui_standard_override_values($form, $form_state);
// Mark the changed section of the view as changed.
// @todo Document why we are doing this, and see if we still need it.
if (!empty($form['#section'])) {
$form_state['view']->changed_sections[$form['#section']] = TRUE;
}
// Based on the user's choice in the display dropdown, determine which display
// these changes apply to.
if ($revert) {
// If it's revert just change the override and return.
$display = &$form_state['view']->display[$form_state['display_id']];
$display->handler->options_override($form, $form_state);
// Don't execute the normal submit handling but still store the changed
// view into cache.
views_ui_cache_set($form_state['view']);
return;
}
elseif ($was_defaulted === $is_defaulted) {
// We're not changing which display these form values apply to.
// Run the regular submit handler for this form.
}
elseif ($was_defaulted && !$is_defaulted) {
// We were using the default display's values, but we're now overriding
// the default display and saving values specific to this display.
$display = &$form_state['view']->display[$form_state['display_id']];
// options_override toggles the override of this section.
$display->handler->options_override($form, $form_state);
$display->handler->options_submit($form, $form_state);
}
elseif (!$was_defaulted && $is_defaulted) {
// We used to have an override for this display, but the user now wants
// to go back to the default display.
// Overwrite the default display with the current form values, and make
// the current display use the new default values.
$display = &$form_state['view']->display[$form_state['display_id']];
// options_override toggles the override of this section.
$display->handler->options_override($form, $form_state);
$display->handler->options_submit($form, $form_state);
}
$submit_handler = $form['#form_id'] . '_submit';
if (function_exists($submit_handler)) {
$submit_handler($form, $form_state);
}
}
/**
* Return the was_defaulted, is_defaulted and revert state of a form.
*/
function views_ui_standard_override_values($form, $form_state) {
// Make sure the dropdown exists in the first place.
if (isset($form_state['values']['override']['dropdown'])) {
// #default_value is used to determine whether it was the default value or
// not. So the available options are: $display, 'default' and
// 'default_revert', not 'defaults'.
$was_defaulted = ($form['override']['dropdown']['#default_value'] === 'defaults');
$is_defaulted = ($form_state['values']['override']['dropdown'] === 'default');
$revert = ($form_state['values']['override']['dropdown'] === 'default_revert');
if ($was_defaulted !== $is_defaulted && isset($form['#section'])) {
// We're changing which display these values apply to.
// Update the #section so it knows what to mark changed.
$form['#section'] = str_replace('default-', $form_state['display_id'] . '-', $form['#section']);
}
}
else {
// The user didn't get the dropdown for overriding the default display.
$was_defaulted = FALSE;
$is_defaulted = FALSE;
$revert = FALSE;
}
return array($was_defaulted, $is_defaulted, $revert);
}
/**
* Submit handler for cancel button.
*/
function views_ui_standard_cancel($form, &$form_state) {
if (!empty($form_state['view']->changed) && isset($form_state['view']->form_cache)) {
unset($form_state['view']->form_cache);
views_ui_cache_set($form_state['view']);
}
$form_state['redirect'] = 'admin/structure/views/view/' . $form_state['view']->name . '/edit';
}
/**
* Add a