Move into nested docroot

This commit is contained in:
Rob Davies 2017-02-13 15:31:17 +00:00
parent 83a0d3a149
commit c8b70abde9
13405 changed files with 0 additions and 0 deletions

View file

@ -0,0 +1,322 @@
<?php
/**
* @file
* Provides the Views' administrative interface.
*/
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
/**
* 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 $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 $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 $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\Component\Utility\Html::getUniqueId() 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';
$element_info = \Drupal::service('element_info');
$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->getValues().
'#process' => array_merge(array('views_ui_add_limited_validation'), $element_info->getInfoProperty('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->getInfoProperty('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. We also cast the #title to string as we will use it as an array
// key and it may be a TranslatableMarkup.
$button_title = !empty($triggering_element['#title']) ? (string) $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,
);
$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-JavaScript fallback submit button to limit its validation errors.
*/
function views_ui_add_limited_validation($element, FormStateInterface $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 = NestedArray::getValue($form_state->getCompleteForm(), $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 \Drupal::formBuilder()->doBuildForm()
// will have already copied it to $form_state->getTriggeringElement() 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.
$clicked_button = &$form_state->getTriggeringElement();
if ($clicked_button && $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 (for 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, FormStateInterface $form_state) {
// 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->getCompleteForm();
$refresh_parents = $element['#views_ui_ajax_data']['refresh_parents'];
$refresh_element = NestedArray::getValue($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 <div> here.
$id = $element['#views_ui_ajax_data']['wrapper'];
$refresh_element += array(
'#prefix' => '',
'#suffix' => '',
);
$refresh_element['#prefix'] = '<div id="' . $id . '" class="views-ui-ajax-wrapper">' . $refresh_element['#prefix'];
$refresh_element['#suffix'] .= '</div>';
// Copy the element that needs to be refreshed back into the form, with our
// modifications to it.
NestedArray::setValue($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, FormStateInterface $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 NestedArray::getValue($form, $form_state->getTriggeringElement()['#views_ui_ajax_data']['refresh_parents']);
}
/**
* Non-Javascript fallback for updating the add view form.
*/
function views_ui_nojs_submit($form, FormStateInterface $form_state) {
$form_state->setRebuild();
}
/**
* Add a <select> dropdown for a given section, allowing the user to
* change whether this info is stored on the default display or on
* the current display.
*/
function views_ui_standard_display_dropdown(&$form, FormStateInterface $form_state, $section) {
$view = $form_state->get('view');
$display_id = $form_state->get('display_id');
$executable = $view->getExecutable();
$displays = $executable->displayHandlers;
$current_display = $executable->display_handler;
// @todo Move this to a separate function if it's needed on any forms that
// don't have the display dropdown.
$form['override'] = array(
'#prefix' => '<div class="views-override clearfix form--inline views-offset-top" data-drupal-views-offset="top">',
'#suffix' => '</div>',
'#weight' => -1000,
'#tree' => TRUE,
);
// Add the "2 of 3" progress indicator.
if ($form_progress = $view->getFormProgress()) {
$form['progress']['#markup'] = '<div id="views-progress-indicator" class="views-progress-indicator">' . t('@current of @total', array('@current' => $form_progress['current'], '@total' => $form_progress['total'])) . '</div>';
$form['progress']['#weight'] = -1001;
}
// The dropdown should not be added when :
// - this is the default display.
// - there is no master shown and just one additional display (mostly page)
// and the current display is defaulted.
if ($current_display->isDefaultDisplay() || ($current_display->isDefaulted($section) && !\Drupal::config('views.settings')->get('ui.show.master_display') && count($displays) <= 2)) {
return;
}
// Determine whether any other displays have overrides for this section.
$section_overrides = FALSE;
$section_defaulted = $current_display->isDefaulted($section);
foreach ($displays as $id => $display) {
if ($id === 'default' || $id === $display_id) {
continue;
}
if ($display && !$display->isDefaulted($section)) {
$section_overrides = TRUE;
}
}
$display_dropdown['default'] = ($section_overrides ? t('All displays (except overridden)') : t('All displays'));
$display_dropdown[$display_id] = t('This @display_type (override)', array('@display_type' => $current_display->getPluginId()));
// Only display the revert option if we are in a overridden section.
if (!$section_defaulted) {
$display_dropdown['default_revert'] = t('Revert to default');
}
$form['override']['dropdown'] = array(
'#type' => 'select',
'#title' => t('For'), // @TODO: Translators may need more context than this.
'#options' => $display_dropdown,
);
if ($current_display->isDefaulted($section)) {
$form['override']['dropdown']['#default_value'] = 'defaults';
}
else {
$form['override']['dropdown']['#default_value'] = $display_id;
}
}
/**
* Create the menu path for one of our standard AJAX forms based upon known
* information about the form.
*
* @return \Drupal\Core\Url
* The URL object pointing to the form URL.
*/
function views_ui_build_form_url(FormStateInterface $form_state) {
$ajax = !$form_state->get('ajax') ? 'nojs' : 'ajax';
$name = $form_state->get('view')->id();
$form_key = $form_state->get('form_key');
$display_id = $form_state->get('display_id');
$form_key = str_replace('-', '_', $form_key);
$route_name = "views_ui.form_{$form_key}";
$route_parameters = [
'js' => $ajax,
'view' => $name,
'display_id' => $display_id
];
$url = Url::fromRoute($route_name, $route_parameters);
if ($type = $form_state->get('type')) {
$url->setRouteParameter('type', $type);
}
if ($id = $form_state->get('id')) {
$url->setRouteParameter('id', $id);
}
return $url;
}
/**
* #process callback for a button; determines if a button is the form's triggering element.
*
* The Form API has logic to determine the form's triggering element based on
* the data in POST. However, it only checks buttons based on a single #value
* per button. This function may be added to a button's #process callbacks to
* extend button click detection to support multiple #values per button. If the
* data in POST matches any value in the button's #values array, then the
* button is detected as having been clicked. This can be used when the value
* (label) of the same logical button may be different based on context (e.g.,
* "Apply" vs. "Apply and continue").
*
* @see _form_builder_handle_input_element()
* @see _form_button_was_clicked()
*/
function views_ui_form_button_was_clicked($element, FormStateInterface $form_state) {
$user_input = $form_state->getUserInput();
$process_input = empty($element['#disabled']) && ($form_state->isProgrammed() || ($form_state->isProcessingInput() && (!isset($element['#access']) || $element['#access'])));
if ($process_input && !$form_state->getTriggeringElement() && !empty($element['#is_button']) && isset($user_input[$element['#name']]) && isset($element['#values']) && in_array($user_input[$element['#name']], array_map('strval', $element['#values']), TRUE)) {
$form_state->setTriggeringElement($element);
}
return $element;
}

View file

@ -0,0 +1,94 @@
langcode: en
status: true
dependencies:
module:
- views_ui
id: views-ui
label: 'View edit page'
module: views_ui
routes:
-
route_name: entity.view.edit_form
-
route_name: entity.view.edit_display_form
tips:
views-main:
id: views-main
plugin: text
label: 'Manage view settings'
body: 'View or edit the configuration.'
weight: 1
views-ui-displays:
id: views-ui-displays
plugin: text
label: 'Displays in this view'
body: 'A display is a way of outputting the results, e.g., as a page or a block. A view can contain multiple displays, which are listed here. The active display is highlighted.'
weight: 2
attributes:
data-id: views-display-top
views-ui-view-admin:
id: views-ui-view-admin
plugin: text
label: 'View administration'
body: 'Perform administrative tasks, including adding a description and creating a clone. Click the drop-down button to view the available options.'
weight: 3
location: left
attributes:
data-id: views-display-extra-actions
views-ui-format:
id: views-ui-format
plugin: text
label: 'Output format'
body: 'Choose how to output results. E.g., choose <em>Content</em> to output each item completely, using your configured display settings. Or choose <em>Fields</em>, which allows you to output only specific fields for each result. Additional formats can be added by installing modules to <em>extend</em> Drupal''s base functionality.'
weight: 4
attributes:
data-class: views-ui-display-tab-bucket.format
views-ui-fields:
id: views-ui-fields
plugin: text
label: Fields
body: 'If this view uses fields, they are listed here. You can click on a field to configure it.'
weight: 5
attributes:
data-class: views-ui-display-tab-bucket.field
views-ui-filter:
id: views-ui-filter
plugin: text
label: 'Filter your view'
body: 'Add filters to limit the results in the output. E.g., to only show content that is <em>published</em>, you would add a filter for <em>Published</em> and select <em>Yes</em>.'
weight: 6
attributes:
data-class: views-ui-display-tab-bucket.filter
views-ui-filter-operations:
id: views-ui-filter-operations
plugin: text
label: 'Filter actions'
body: 'Add, rearrange or remove filters.'
weight: 7
attributes:
data-class: 'views-ui-display-tab-bucket.filter .dropbutton-widget'
views-ui-sorts:
id: views-ui-sorts
plugin: text
label: 'Sort Criteria'
body: 'Control the order in which the results are output. Click on an active sort rule to configure it.'
weight: 8
attributes:
data-class: views-ui-display-tab-bucket.sort
views-ui-sorts-operations:
id: views-ui-sorts-operations
plugin: text
label: 'Sort actions'
body: 'Add, rearrange or remove sorting rules.'
weight: 9
attributes:
data-class: 'views-ui-display-tab-bucket.sort .dropbutton-widget'
views-ui-preview:
id: views-ui-preview
plugin: text
label: Preview
body: 'Show a preview of the view output.'
weight: 10
location: left
attributes:
data-id: preview-submit

View file

@ -0,0 +1,208 @@
/**
* @file
* The .admin.css file is intended to only contain positioning and size
* declarations. For example: display, position, float, clear, and overflow.
*/
.views-admin ul,
.views-admin menu,
.views-admin dir {
padding: 0;
}
.views-admin pre {
margin-bottom: 0;
margin-top: 0;
white-space: pre-wrap;
}
.views-left-25 {
float: left; /* LTR */
width: 25%;
}
[dir="rtl"] .views-left-25 {
float: right;
}
.views-left-30 {
float: left; /* LTR */
width: 30%;
}
[dir="rtl"] .views-left-30 {
float: right;
}
.views-left-40 {
float: left; /* LTR */
width: 40%;
}
[dir="rtl"] .views-left-40 {
float: right;
}
.views-left-50 {
float: left; /* LTR */
width: 50%;
}
[dir="rtl"] .views-left-50 {
float: right;
}
.views-left-75 {
float: left; /* LTR */
width: 75%;
}
[dir="rtl"] .views-left-75 {
float: right;
}
.views-right-50 {
float: right; /* LTR */
width: 50%;
}
[dir="rtl"] .views-right-50 {
float: left;
}
.views-right-60 {
float: right; /* LTR */
width: 60%;
}
[dir="rtl"] .views-right-60 {
float: left;
}
.views-right-70 {
float: right; /* LTR */
width: 70%;
}
[dir="rtl"] .views-right-70 {
float: left;
}
.views-group-box .form-item {
margin-left: 3px;
margin-right: 3px;
}
/*
* The attachment details section, its tabs for each section and the buttons
* to add a new section
*/
.views-displays {
clear: both;
}
/* The tabs that switch between sections */
.views-displays .tabs {
border-bottom: 0 none;
margin: 0;
overflow: visible;
padding: 0;
}
.views-displays .tabs > li {
border-right: 0 none; /* LTR */
float: left; /* LTR */
padding: 0;
}
[dir="rtl"] .views-displays .tabs > li {
float: right;
border-left: 0 none;
border-right: 1px solid #bfbfbf;
}
.views-displays .tabs .open > a {
position: relative;
z-index: 51;
}
.views-displays .tabs .views-display-deleted-link {
text-decoration: line-through;
}
.views-display-deleted > details > summary,
.views-display-deleted .details-wrapper > .views-ui-display-tab-bucket > *,
.views-display-deleted .views-display-columns {
opacity: 0.25;
}
.views-display-disabled > details > summary,
.views-display-disabled .details-wrapper > .views-ui-display-tab-bucket > *,
.views-display-disabled .views-display-columns {
opacity: 0.5;
}
.views-display-tab .details-wrapper > .views-ui-display-tab-bucket .actions {
opacity: 1.0;
}
.views-displays .tabs .add {
position: relative;
}
.views-displays .tabs .action-list {
left: 0; /* LTR */
margin: 0;
position: absolute;
top: 23px;
z-index: 50;
}
[dir="rtl"] .views-displays .tabs .action-list {
left: auto;
right: 0;
}
.views-displays .tabs .action-list li {
display: block;
}
.views-display-columns .details-wrapper {
padding: 0;
}
.views-display-column {
box-sizing: border-box;
}
.views-display-columns > * {
margin-bottom: 2em;
}
@media screen and (min-width:45em) { /* 720px */
.views-display-columns > * {
float: left; /* LTR */
margin-left: 2%; /* LTR */
margin-bottom: 0;
width: 32%;
}
[dir="rtl"] .views-display-columns > * {
float: right;
margin-left: 0;
margin-right: 2%;
}
.views-display-columns > *:first-child {
margin-left: 0; /* LTR */
}
[dir="rtl"] .views-display-columns > *:first-child {
margin-right: 0;
}
}
.views-ui-dialog .scroll {
overflow: auto;
padding: 1em;
}
.views-filterable-options-controls {
display: none;
}
.views-ui-dialog .views-filterable-options-controls {
display: inline;
}
/* Don't let the messages overwhelm the modal */
.views-ui-dialog .views-messages {
max-height: 200px;
overflow: auto;
}
.views-display-setting .label,
.views-display-setting .views-ajax-link {
float: left; /* LTR */
}
[dir="rtl"] .views-display-setting .label,
[dir="rtl"] .views-display-setting .views-ajax-link {
float: right;
}
.form-item-options-value-all {
display: none;
}
.js-only {
display: none;
}
html.js .js-only {
display: inherit;
}
html.js span.js-only {
display: inline;
}
.js .views-edit-view .dropbutton-wrapper {
width: auto;
}

View file

@ -0,0 +1,838 @@
/**
* @file
* The .admin.theme.css file is intended to contain presentation declarations
* including images, borders, colors, and fonts.
*/
.views-admin .links {
list-style: none outside none;
margin: 0;
}
.views-admin a:hover {
text-decoration: none;
}
.box-padding {
padding-left: 12px;
padding-right: 12px;
}
.box-margin {
margin: 12px 12px 0 12px;
}
.views-admin .icon {
height: 16px;
width: 16px;
}
.views-admin .icon,
.views-admin .icon-text {
background-attachment: scroll;
background-image: url(../images/sprites.png);
background-position: left top; /* LTR */
background-repeat: no-repeat;
}
[dir="rtl"] .views-admin .icon,
[dir="rtl"] .views-admin .icon-text {
background-position: right top;
}
.views-admin a.icon {
background: linear-gradient(-90deg, #fff 0, #e8e8e8 100%) no-repeat, repeat-y;
border: 1px solid #ddd;
border-radius: 4px;
box-shadow: 0 0 0 rgba(0,0,0,0.3333) inset;
}
.views-admin a.icon:hover {
border-color: #d0d0d0;
box-shadow: 0 0 1px rgba(0,0,0,0.3333) inset;
}
.views-admin a.icon:active {
border-color: #c0c0c0;
}
.views-admin span.icon {
float: left; /* LTR */
position: relative;
}
[dir="rtl"] .views-admin span.icon {
float: right;
}
.views-admin .icon.compact {
display: block;
overflow: hidden;
direction: ltr;
text-indent: -9999px;
}
/* Targets any element with an icon -> text combo */
.views-admin .icon-text {
padding-left: 19px; /* LTR */
}
[dir="rtl"] .views-admin .icon-text {
padding-left: 0;
padding-right: 19px;
}
.views-admin .icon.linked {
background-position: center -153px;
}
.views-admin .icon.unlinked {
background-position: center -195px;
}
.views-admin .icon.add {
background-position: center 3px;
}
.views-admin a.icon.add {
background-position: center 3px, left top; /* LTR */
}
[dir="rtl"] .views-admin a.icon.add {
background-position: center 3px, right top;
}
.views-admin .icon.delete {
background-position: center -52px;
}
.views-admin a.icon.delete {
background-position: center -52px, left top; /* LTR */
}
[dir="rtl"] .views-admin a.icon.delete {
background-position: center -52px, right top;
}
.views-admin .icon.rearrange {
background-position: center -111px;
}
.views-admin a.icon.rearrange {
background-position: center -111px, left top; /* LTR */
}
[dir="rtl"] .views-admin a.icon.rearrange {
background-position: center -111px, right top;
}
.views-displays .tabs a:hover > .icon.add {
background-position: center -25px;
}
.views-displays .tabs .open a:hover > .icon.add {
background-position: center 3px;
}
details.box-padding {
border: none;
}
.views-admin details details {
margin-bottom: 0;
}
.form-item {
margin-top: 9px;
padding-bottom: 0;
padding-top: 0;
}
.form-type-checkbox {
margin-top: 6px;
}
.form-checkbox,
.form-radio {
vertical-align: baseline;
}
.container-inline {
padding-top: 15px;
padding-bottom: 15px;
}
.container-inline > * + *,
.container-inline .details-wrapper > * + * {
padding-left: 4px; /* LTR */
}
[dir="rtl"] .container-inline > * + *,
[dir="rtl"] .container-inline .details-wrapper > * + * {
padding-left: 0;
padding-right: 4px;
}
.views-admin details details.container-inline {
margin-bottom: 1em;
margin-top: 1em;
padding-top: 0;
}
.views-admin details details.container-inline > .details-wrapper {
padding-bottom: 0;
}
/* Indent form elements so they're directly underneath the label of the checkbox that reveals them */
.views-admin .form-type-checkbox + .form-wrapper {
margin-left: 16px; /* LTR */
}
[dir="rtl"] .views-admin .form-type-checkbox + .form-wrapper {
margin-left: 0;
margin-right: 16px;
}
/* Hide 'remove' checkboxes. */
.views-remove-checkbox {
display: none;
}
/* sizes the labels of checkboxes and radio button to the height of the text */
.views-admin .form-type-checkbox label,
.views-admin .form-type-radio label {
line-height: 2;
}
.views-admin-dependent .form-item {
margin-bottom: 6px;
margin-top: 6px;
}
.views-ui-view-title {
font-weight: bold;
margin-top: 0;
}
.view-changed {
margin-bottom: 21px;
}
.views-admin .unit-title {
font-size: 15px;
line-height: 1.6154;
margin-bottom: 0;
margin-top: 18px;
}
/* These header classes are ambiguous and should be scoped to th elements */
.views-ui-name {
width: 18%;
}
.views-ui-description {
width: 26%;
}
.views-ui-tag {
width: 8%;
}
.views-ui-path {
width: auto;
}
.views-ui-operations {
width: 24%;
}
/**
* I wish this didn't have to be so specific
*/
.form-item-description-enable + .form-item-description {
margin-top: 0;
}
.form-item-description-enable label {
font-weight: bold;
}
.form-item-page-create,
.form-item-block-create {
margin-top: 13px;
}
.form-item-page-create label,
.form-item-block-create label,
.form-item-rest-export-create label {
font-weight: bold;
}
/* This makes the form elements after the "Display Format" label flow underneath the label */
.form-item-page-style-style-plugin > label,
.form-item-block-style-style-plugin > label {
display: block;
}
.views-attachment .options-set label {
font-weight: normal;
}
/* Styling for the form that allows views filters to be rearranged. */
.group-populated {
display: none;
}
td.group-title {
font-weight: bold;
}
.views-ui-dialog td.group-title {
margin: 0;
padding: 0;
}
.views-ui-dialog td.group-title span {
display: block;
height: 1px;
overflow: hidden;
}
.group-message .form-submit,
.views-remove-group-link,
.views-add-group {
float: right; /* LTR */
clear: both;
}
[dir="rtl"] .group-message .form-submit,
[dir="rtl"] .views-remove-group-link,
[dir="rtl"] .views-add-group {
float: left;
}
.views-operator-label {
font-style: italic;
font-weight: bold;
padding-left: 0.5em; /* LTR */
text-transform: uppercase;
}
[dir="rtl"] .views-operator-label {
padding-left: 0;
padding-right: 0.5em;
}
.grouped-description,
.exposed-description {
float: left; /* LTR */
padding-top: 3px;
padding-right: 10px; /* LTR */
}
[dir="rtl"] .grouped-description,
[dir="rtl"] .exposed-description {
float: right;
padding-left: 10px;
padding-right: 0;
}
.views-displays {
border: 1px solid #ccc;
padding-bottom: 36px;
}
.views-display-top {
background-color: #e1e2dc;
border-bottom: 1px solid #ccc;
padding: 8px 8px 3px;
position: relative;
}
.views-display-top .tabs {
margin-right: 18em; /* LTR */
}
[dir="rtl"] .views-display-top .tabs {
margin-left: 18em;
margin-right: 0;
}
.views-display-top .tabs > li {
margin-right: 6px; /* LTR */
padding-left: 0; /* LTR */
}
[dir="rtl"] .views-display-top .tabs > li {
margin-left: 6px;
margin-right: 0.3em;
padding-right: 0;
}
.views-display-top .tabs > li:last-child {
margin-right: 0; /* LTR */
}
[dir="rtl"] .views-display-top .tabs > li:last-child {
margin-left: 0;
margin-right: 0.3em;
}
.form-edit .form-actions {
background-color: #e1e2dc;
border-right: 1px solid #ccc;
border-bottom: 1px solid #ccc;
border-left: 1px solid #ccc;
margin-top: 0;
padding: 8px 12px;
}
.views-displays .tabs.secondary {
margin-right: 200px; /* LTR */
border: 0;
}
[dir="rtl"] .views-displays .tabs.secondary {
margin-left: 200px;
margin-right: 0;
}
.views-displays .tabs.secondary li,
.views-displays .tabs.secondary li.is-active {
background: transparent;
border: 0;
padding: 0;
width: auto;
}
.views-displays .tabs li.add ul.action-list li{
margin: 0;
}
.views-displays .tabs.secondary li {
margin: 0 5px 5px 6px; /* LTR */
}
[dir="rtl"] .views-displays .tabs.secondary li {
margin-left: 5px;
margin-right: 6px;
}
.views-displays .tabs.secondary .tabs__tab + .tabs__tab {
border-top: 0;
}
.views-displays .tabs li.tabs__tab:hover {
border: 0;
padding-left: 0; /* LTR */
}
[dir="rtl"] .views-displays .tabs li.tabs__tab:hover {
padding-right: 0;
}
.views-displays .tabs.secondary a {
border: 1px solid #cbcbcb;
border-radius: 7px;
display: inline-block;
font-size: small;
line-height: 1.3333;
padding: 3px 7px;
}
/* Display a red border if the display doesn't validate. */
.views-displays .tabs li.is-active a.is-active.error,
.views-displays .tabs .error {
border: 2px solid #ed541d;
padding: 1px 6px;
}
.views-displays .tabs a:focus {
outline: none;
text-decoration: underline;
}
.views-displays .tabs.secondary li a {
background-color: #fff;
}
.views-displays .tabs li a:hover,
.views-displays .tabs li.is-active a,
.views-displays .tabs li.is-active a.is-active {
background-color: #555;
color: #fff;
}
.views-displays .tabs .open > a {
background-color: #f1f1f1;
border-bottom: 1px solid transparent;
position: relative;
}
.views-displays .tabs .open > a:hover {
color: #0074bd;
background-color: #f1f1f1;
}
.views-displays .tabs .action-list li {
background-color: #f1f1f1;
border-color: #cbcbcb;
border-style: solid;
border-width: 0 1px;
padding: 2px 9px;
}
.views-displays .tabs .action-list li:first-child {
border-width: 1px 1px 0;
}
.views-displays .action-list li:last-child {
border-width: 0 1px 1px;
}
.views-displays .tabs .action-list li:last-child {
border-width: 0 1px 1px;
}
.views-displays .tabs .action-list input.form-submit {
background: none repeat scroll 0 0 transparent;
border: medium none;
margin: 0;
padding: 0;
}
.views-displays .tabs .action-list input.form-submit:hover {
box-shadow: none;
}
.views-displays .tabs .action-list li:hover {
background-color: #ddd;
}
.edit-display-settings {
margin: 12px 12px 0 12px
}
.edit-display-settings-top.views-ui-display-tab-bucket {
border: 1px solid #f3f3f3;
line-height: 20px;
margin: 0 0 15px 0;
padding-top: 4px;
padding-bottom: 4px;
position: relative;
}
.views-display-column {
border: 1px solid #f3f3f3;
}
.views-display-column + .views-display-column {
margin-top: 0;
}
.view-preview-form .form-item-view-args,
.view-preview-form .form-actions {
margin-top: 5px;
}
.view-preview-form .arguments-preview {
font-size: 1em;
}
.view-preview-form .arguments-preview,
.view-preview-form .form-item-view-args {
margin-left: 10px; /* LTR */
}
[dir="rtl"] .view-preview-form .arguments-preview,
[dir="rtl"] .view-preview-form .form-item-view-args {
margin-left: 0;
margin-right: 10px;
}
.view-preview-form .form-item-view-args label {
float: left; /* LTR */
font-weight: normal;
height: 6ex;
margin-right: 0.75em; /* LTR */
}
[dir="rtl"] .view-preview-form .form-item-view-args label {
float: right;
margin-left: 0.75em;
margin-right: 0.2em;
}
.form-item-live-preview,
.form-item-view-args,
.preview-submit-wrapper {
display: inline-block;
}
.form-item-live-preview,
.view-preview-form .form-actions {
vertical-align: top;
}
@media screen and (min-width:45em) { /* 720px */
.view-preview-form .form-type-textfield .description {
white-space: nowrap;
}
}
/* These are the individual "buckets," or boxes, inside the display settings area */
.views-ui-display-tab-bucket {
border-bottom: 1px solid #f3f3f3;
line-height: 20px;
margin: 0;
padding-top: 4px;
position: relative;
}
.views-ui-display-tab-bucket:last-of-type {
border-bottom: none;
}
.views-ui-display-tab-bucket + .views-ui-display-tab-bucket {
border-top: medium none;
}
.views-ui-display-tab-bucket__title,
.views-ui-display-tab-bucket > .views-display-setting {
padding: 2px 6px 4px;
}
.views-ui-display-tab-bucket__title {
font-size: small;
margin: 0;
}
.views-ui-display-tab-bucket.access {
padding-top: 0;
}
.views-ui-display-tab-bucket.page-settings {
border-bottom: medium none;
}
.views-display-setting .views-ajax-link {
margin-left: 0.2083em;
margin-right: 0.2083em;
}
.views-ui-display-tab-setting > span {
margin-left: 0.5em; /* LTR */
}
[dir="rtl"] .views-ui-display-tab-setting > span {
margin-left: 0;
margin-right: 0.5em;
}
/** Applies an overridden(italics) font style to overridden buckets.
* The better way to implement this would be to add the overridden class
* to the bucket header when the bucket is overridden and style it as a
* generic icon classed element. For the moment, we'll style the bucket
* header specifically with the overridden font style.
*/
.views-ui-display-tab-setting.overridden,
.views-ui-display-tab-bucket.overridden .views-ui-display-tab-bucket__title {
font-style: italic;
}
/* This is each row within one of the "boxes." */
.views-ui-display-tab-bucket .views-display-setting {
color: #666;
font-size: 12px;
padding-bottom: 2px;
}
.views-ui-display-tab-bucket .views-display-setting:nth-of-type(even) {
background-color: #f3f5ee;
}
.views-ui-display-tab-actions.views-ui-display-tab-bucket .views-display-setting {
background-color: transparent;
}
.views-ui-display-tab-bucket .views-group-text {
margin-top: 6px;
margin-bottom: 6px;
}
.views-display-setting .label {
margin-right: 3px; /* LTR */
}
[dir="rtl"] .views-display-setting .label {
margin-left: 3px;
margin-right: 0;
}
.views-edit-view {
margin-bottom: 15px;
}
/* The contents of the popup dialog on the views edit form. */
.views-filterable-options .form-type-checkbox {
padding: 5px 8px;
border-top: none;
}
.views-filterable-options {
border-top: 1px solid #ccc;
}
.filterable-option .form-item {
margin-bottom: 0;
margin-top: 0;
}
.views-filterable-options .filterable-option .title {
font-weight: bold;
cursor: pointer;
}
.views-filterable-options .form-type-checkbox .description {
margin-top: 0;
margin-bottom: 0;
}
.views-filterable-options-controls .form-item {
width: 30%;
margin: 0 0 0 2%; /* LTR */
}
[dir="rtl"] .views-filterable-options-controls .form-item {
margin: 0 2% 0 0;
}
.views-filterable-options-controls input,
.views-filterable-options-controls select {
width: 100%;
}
.views-ui-dialog .ui-dialog-content {
padding: 0;
}
.views-ui-dialog .views-filterable-options {
margin-bottom: 10px;
}
.views-ui-dialog .views-add-form-selected.container-inline {
padding: 0;
}
.views-ui-dialog .views-add-form-selected.container-inline > div {
display: block;
}
.views-ui-dialog .form-item-selected {
margin: 0;
padding: 6px 16px;
}
.views-ui-dialog .views-override {
background-color: #f3f4ee;
padding: 8px 13px;
}
.views-ui-dialog.views-ui-dialog-scroll .ui-dialog-titlebar {
border: none;
}
.views-ui-dialog .views-offset-top {
border-bottom: 1px solid #CCC;
}
.views-ui-dialog .views-offset-bottom {
border-top: 1px solid #CCC;
}
.views-ui-dialog .views-override > * {
margin: 0;
}
.views-ui-dialog .views-progress-indicator {
color: #fff;
font-size: 11px;
position: absolute;
right: 10px; /* LTR */
top: 32px;
}
[dir="rtl"] .views-ui-dialog .views-progress-indicator {
left: 10px;
right: auto;
}
.views-ui-dialog .views-progress-indicator:before {
content: "\003C\00A0";
}
.views-ui-dialog .views-progress-indicator:after {
content: "\00A0\003E";
}
.views-ui-dialog details .item-list {
padding-left: 2em; /* LTR */
}
[dir="rtl"] .views-ui-dialog details .item-list {
padding-left: 0;
padding-right: 2em;
}
.views-ui-rearrange-filter-form table {
border-collapse: collapse;
}
.views-ui-rearrange-filter-form tr td[rowspan] {
border-color: #cdcdcd;
border-style: solid;
border-width: 0 1px 1px 1px;
}
.views-ui-rearrange-filter-form tr[id^="views-row"] {
border-right: 1px solid #cdcdcd; /* LTR */
}
[dir="rtl"] .views-ui-rearrange-filter-form tr[id^="views-row"] {
border-left: 1px solid #cdcdcd;
border-right: 0;
}
.views-ui-rearrange-filter-form .even td {
background-color: #f3f4ed;
}
.views-ui-rearrange-filter-form .views-group-title {
border-top: 1px solid #cdcdcd;
}
.views-ui-rearrange-filter-form .group-empty {
border-bottom: 1px solid #cdcdcd;
}
.form-item-options-expose-required,
.form-item-options-expose-label,
.form-item-options-expose-description {
margin-bottom: 6px;
margin-left: 18px; /* LTR */
margin-top: 6px;
}
[dir="rtl"] .form-item-options-expose-required,
[dir="rtl"] .form-item-options-expose-label,
[dir="rtl"] .form-item-options-expose-description {
margin-left: 0;
margin-right: 18px;
}
.views-preview-wrapper {
border: 1px solid #ccc;
}
.view-preview-form {
position: relative;
}
.view-preview-form__title {
background-color: #e1e2dc;
border-bottom: 1px solid #ccc;
margin-top: 0;
padding: 8px 12px;
}
.view-preview-form .form-item-live-preview {
position: absolute;
right: 12px;
top: 3px;
margin-top: 2px;
margin-left: 2px; /* LTR */
}
[dir="rtl"] .view-preview-form .form-item-live-preview {
right: auto;
left: 12px;
margin-left: 0;
margin-right: 2px;
}
.views-live-preview {
padding: 12px;
}
.views-live-preview .views-query-info {
overflow: auto;
}
.views-live-preview .section-title {
color: #818181;
display: inline-block;
font-size: 13px;
font-weight: normal;
line-height: 1.6154;
margin-bottom: 0;
margin-top: 0;
}
.views-live-preview .view > * {
margin-top: 18px;
}
.views-live-preview .preview-section {
border: 1px dashed #dedede;
margin: 0 -5px;
padding: 3px 5px;
}
.views-live-preview li.views-row + li.views-row {
margin-top: 18px;
}
/* The div.views-row is intentional and excludes li.views-row, for example */
.views-live-preview div.views-row + div.views-row {
margin-top: 36px;
}
.views-query-info table {
border-collapse: separate;
border-color: #ddd;
border-spacing: 0;
margin: 10px 0;
}
.views-query-info table tr {
background-color: #f9f9f9;
}
.views-query-info table th,
.views-query-info table td {
color: #666;
padding: 4px 10px;
}
.messages {
margin-bottom: 18px;
line-height: 1.4555;
}
.dropbutton-multiple {
position: absolute;
}
.dropbutton-widget {
position: relative;
}
.js .views-edit-view .dropbutton-wrapper .dropbutton .dropbutton-action > * {
font-size: 10px;
}
.js .dropbutton-wrapper .dropbutton .dropbutton-action > .ajax-progress-throbber {
position: absolute;
right: -5px; /* LTR */
top: -1px;
z-index: 2;
}
[dir="rtl"].js .dropbutton-wrapper .dropbutton .dropbutton-action > .ajax-progress-throbber {
left: -5px;
right: auto;
}
.js .dropbutton-wrapper.dropbutton-multiple.open .dropbutton-action:first-child a {
border-radius: 1.1em 0 0 0; /* LTR */
}
[dir="rtl"].js .dropbutton-wrapper.dropbutton-multiple.open .dropbutton-action:first-child a {
border-radius: 0 1.1em 0 0;
}
.js .dropbutton-wrapper.dropbutton-multiple.open .dropbutton-action:last-child a {
border-radius: 0 0 0 1.1em; /* LTR */
}
[dir="rtl"].js .dropbutton-wrapper.dropbutton-multiple.open .dropbutton-action:last-child a {
border-radius: 0 0 1.1em 0;
}
.views-display-top .dropbutton-wrapper {
position: absolute;
right: 12px; /* LTR */
top: 7px;
}
[dir="rtl"] .views-display-top .dropbutton-wrapper {
left: 12px;
right: auto;
}
.views-display-top .dropbutton-wrapper .dropbutton-widget .dropbutton-action a {
width: auto;
}
.views-ui-display-tab-bucket .dropbutton-wrapper {
position: absolute;
right: 5px; /* LTR */
top: 4px;
}
[dir="rtl"] .views-ui-display-tab-bucket .dropbutton-wrapper {
left: 5px;
right: auto;
}
.views-ui-display-tab-bucket .dropbutton-wrapper .dropbutton-widget .dropbutton-action a {
width: auto;
}
.views-ui-display-tab-actions .dropbutton-wrapper li a,
.views-ui-display-tab-actions .dropbutton-wrapper input {
background: none;
border: medium;
font-family: inherit;
font-size: 12px;
padding-left: 12px; /* LTR */
margin-bottom: 0;
}
[dir="rtl"] .views-ui-display-tab-actions .dropbutton-wrapper li a,
[dir="rtl"] .views-ui-display-tab-actions .dropbutton-wrapper input {
padding-left: 0.5em;
padding-right: 12px;
}
.views-ui-display-tab-actions .dropbutton-wrapper input:hover {
background: none;
border: none;
}
.views-list-section {
margin-bottom: 2em;
}
.form-textarea-wrapper,
.form-item-options-content {
width: 100%;
}

View file

@ -0,0 +1,57 @@
/**
* @file
* The .contextual.css file is intended to contain styles that override declarations
* in the Contextual module.
*/
.views-live-preview .contextual-region-active {
outline: medium none;
}
.views-live-preview .contextual {
right: auto; /* LTR */
top: auto;
}
[dir="rtl"] .views-live-preview .contextual {
left: auto;
}
.js .views-live-preview .contextual {
display: inline;
}
.views-live-preview .contextual-links-trigger {
display: block;
}
.contextual .contextual-links {
border-radius: 0 4px 4px 4px; /* LTR */
min-width: 10em;
padding: 6px 6px 9px 6px;
right: auto; /* LTR */
}
[dir="rtl"] .contextual .contextual-links {
border-radius: 4px 0 4px 4px;
left: auto;
}
.contextual-links li a,
.contextual-links li span {
padding-bottom: 0.25em;
padding-right: 0.1667em; /* LTR */
padding-top: 0.25em;
}
[dir="rtl"] .contextual-links li a,
[dir="rtl"] .contextual-links li span {
padding-left: 0.1667em;
padding-right: 0;
}
.contextual-links li span {
font-weight: bold;
}
.contextual-links li a {
margin: 0.25em 0;
padding-left: 1em; /* LTR */
}
[dir="rtl"] .contextual-links li a {
padding-left: 0.1667em;
padding-right: 1em;
}
.contextual-links li a:hover {
background-color: #badbec;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -0,0 +1,252 @@
/**
* @file
* Handles AJAX submission and response in Views UI.
*/
(function ($, Drupal, drupalSettings) {
'use strict';
/**
* Ajax command for highlighting elements.
*
* @param {Drupal.Ajax} [ajax]
* An Ajax object.
* @param {object} response
* The Ajax response.
* @param {string} response.selector
* The selector in question.
* @param {number} [status]
* The HTTP status code.
*/
Drupal.AjaxCommands.prototype.viewsHighlight = function (ajax, response, status) {
$('.hilited').removeClass('hilited');
$(response.selector).addClass('hilited');
};
/**
* Ajax command to set the form submit action in the views modal edit form.
*
* @param {Drupal.Ajax} [ajax]
* An Ajax object.
* @param {object} response
* The Ajax response. Contains .url
* @param {string} [status]
* The XHR status code?
*/
Drupal.AjaxCommands.prototype.viewsSetForm = function (ajax, response, status) {
var $form = $('.js-views-ui-dialog form');
// Identify the button that was clicked so that .ajaxSubmit() can use it.
// We need to do this for both .click() and .mousedown() since JavaScript
// code might trigger either behavior.
var $submit_buttons = $form.find('input[type=submit].js-form-submit, button.js-form-submit').once('views-ajax-submit');
$submit_buttons.on('click mousedown', function () {
this.form.clk = this;
});
$form.once('views-ajax-submit').each(function () {
var $form = $(this);
var element_settings = {
url: response.url,
event: 'submit',
base: $form.attr('id'),
element: this
};
var ajaxForm = Drupal.ajax(element_settings);
ajaxForm.$form = $form;
});
};
/**
* Ajax command to show certain buttons in the views edit form.
*
* @param {Drupal.Ajax} [ajax]
* An Ajax object.
* @param {object} response
* The Ajax response.
* @param {bool} response.changed
* Whether the state changed for the buttons or not.
* @param {number} [status]
* The HTTP status code.
*/
Drupal.AjaxCommands.prototype.viewsShowButtons = function (ajax, response, status) {
$('div.views-edit-view div.form-actions').removeClass('js-hide');
if (response.changed) {
$('div.views-edit-view div.view-changed.messages').removeClass('js-hide');
}
};
/**
* Ajax command for triggering preview.
*
* @param {Drupal.Ajax} [ajax]
* An Ajax object.
* @param {object} [response]
* The Ajax response.
* @param {number} [status]
* The HTTP status code.
*/
Drupal.AjaxCommands.prototype.viewsTriggerPreview = function (ajax, response, status) {
if ($('input#edit-displays-live-preview').is(':checked')) {
$('#preview-submit').trigger('click');
}
};
/**
* Ajax command to replace the title of a page.
*
* @param {Drupal.Ajax} [ajax]
* An Ajax object.
* @param {object} response
* The Ajax response.
* @param {string} response.siteName
* The site name.
* @param {string} response.title
* The new page title.
* @param {number} [status]
* The HTTP status code.
*/
Drupal.AjaxCommands.prototype.viewsReplaceTitle = function (ajax, response, status) {
var doc = document;
// For the <title> element, make a best-effort attempt to replace the page
// title and leave the site name alone. If the theme doesn't use the site
// name in the <title> element, this will fail.
var oldTitle = doc.title;
// Escape the site name, in case it has special characters in it, so we can
// use it in our regex.
var escapedSiteName = response.siteName.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
var re = new RegExp('.+ (.) ' + escapedSiteName);
doc.title = oldTitle.replace(re, response.title + ' $1 ' + response.siteName);
$('h1.page-title').text(response.title);
};
/**
* Get rid of irritating tabledrag messages.
*
* @return {Array}
* An array of messages. Always empty array, to get rid of the messages.
*/
Drupal.theme.tableDragChangedWarning = function () {
return [];
};
/**
* Trigger preview when the "live preview" checkbox is checked.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches behavior to trigger live preview if the live preview option is
* checked.
*/
Drupal.behaviors.livePreview = {
attach: function (context) {
$('input#edit-displays-live-preview', context).once('views-ajax').on('click', function () {
if ($(this).is(':checked')) {
$('#preview-submit').trigger('click');
}
});
}
};
/**
* Sync preview display.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches behavior to sync the preview display when needed.
*/
Drupal.behaviors.syncPreviewDisplay = {
attach: function (context) {
$('#views-tabset a').once('views-ajax').on('click', function () {
var href = $(this).attr('href');
// Cut of #views-tabset.
var display_id = href.substr(11);
// Set the form element.
$('#views-live-preview #preview-display-id').val(display_id);
});
}
};
/**
* Ajax behaviors for the views_ui module.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches ajax behaviors to the elements with the classes in question.
*/
Drupal.behaviors.viewsAjax = {
collapseReplaced: false,
attach: function (context, settings) {
var base_element_settings = {
event: 'click',
progress: {type: 'fullscreen'}
};
// Bind AJAX behaviors to all items showing the class.
$('a.views-ajax-link', context).once('views-ajax').each(function () {
var element_settings = base_element_settings;
element_settings.base = base;
element_settings.element = this;
// Set the URL to go to the anchor.
if ($(this).attr('href')) {
element_settings.url = $(this).attr('href');
}
var base = $(this).attr('id');
Drupal.ajax(element_settings);
});
$('div#views-live-preview a')
.once('views-ajax').each(function () {
// We don't bind to links without a URL.
if (!$(this).attr('href')) {
return true;
}
var element_settings = base_element_settings;
// Set the URL to go to the anchor.
element_settings.url = $(this).attr('href');
if (Drupal.Views.getPath(element_settings.url).substring(0, 21) !== 'admin/structure/views') {
return true;
}
element_settings.wrapper = 'views-preview-wrapper';
element_settings.method = 'replaceWith';
element_settings.base = base;
element_settings.element = this;
var base = $(this).attr('id');
Drupal.ajax(element_settings);
});
// Within a live preview, make exposed widget form buttons re-trigger the
// Preview button.
// @todo Revisit this after fixing Views UI to display a Preview outside
// of the main Edit form.
$('div#views-live-preview input[type=submit]')
.once('views-ajax').each(function (event) {
$(this).on('click', function () {
this.form.clk = this;
return true;
});
var element_settings = base_element_settings;
// Set the URL to go to the anchor.
element_settings.url = $(this.form).attr('action');
if (Drupal.Views.getPath(element_settings.url).substring(0, 21) !== 'admin/structure/views') {
return true;
}
element_settings.wrapper = 'views-preview-wrapper';
element_settings.method = 'replaceWith';
element_settings.event = 'click';
element_settings.base = base;
element_settings.element = this;
var base = $(this).attr('id');
Drupal.ajax(element_settings);
});
}
};
})(jQuery, Drupal, drupalSettings);

View file

@ -0,0 +1,58 @@
/**
* @file
* Views dialog behaviors.
*/
(function ($, Drupal, drupalSettings) {
'use strict';
function handleDialogResize(e) {
var $modal = $(e.currentTarget);
var $viewsOverride = $modal.find('[data-drupal-views-offset]');
var $scroll = $modal.find('[data-drupal-views-scroll]');
var offset = 0;
var modalHeight;
if ($scroll.length) {
// Add a class to do some styles adjustments.
$modal.closest('.views-ui-dialog').addClass('views-ui-dialog-scroll');
// Let scroll element take all the height available.
$scroll.css({overflow: 'visible', height: 'auto'});
modalHeight = $modal.height();
$viewsOverride.each(function () { offset += $(this).outerHeight(); });
// Take internal padding into account.
var scrollOffset = $scroll.outerHeight() - $scroll.height();
$scroll.height(modalHeight - offset - scrollOffset);
// Reset scrolling properties.
$modal.css('overflow', 'hidden');
$scroll.css('overflow', 'auto');
}
}
/**
* Functionality for views modals.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches modal functionality for views.
* @prop {Drupal~behaviorDetach} detach
* Detaches the modal functionality.
*/
Drupal.behaviors.viewsModalContent = {
attach: function (context) {
$('body').once('viewsDialog').on('dialogContentResize.viewsDialog', '.ui-dialog-content', handleDialogResize);
// When expanding details, make sure the modal is resized.
$(context).find('.scroll').once('detailsUpdate').on('click', 'summary', function (e) {
$(e.currentTarget).trigger('dialogContentResize');
});
},
detach: function (context, settings, trigger) {
if (trigger === 'unload') {
$('body').removeOnce('viewsDialog').off('.viewsDialog');
}
}
};
})(jQuery, Drupal, drupalSettings);

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,54 @@
/**
* @file
* Views listing behaviors.
*/
(function ($, Drupal) {
'use strict';
/**
* Filters the view listing tables by a text input search string.
*
* Text search input: input.views-filter-text
* Target table: input.views-filter-text[data-table]
* Source text: [data-drupal-selector="views-table-filter-text-source"]
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches the filter functionality to the views admin text search field.
*/
Drupal.behaviors.viewTableFilterByText = {
attach: function (context, settings) {
var $input = $('input.views-filter-text').once('views-filter-text');
var $table = $($input.attr('data-table'));
var $rows;
function filterViewList(e) {
var query = $(e.target).val().toLowerCase();
function showViewRow(index, row) {
var $row = $(row);
var $sources = $row.find('[data-drupal-selector="views-table-filter-text-source"]');
var textMatch = $sources.text().toLowerCase().indexOf(query) !== -1;
$row.closest('tr').toggle(textMatch);
}
// Filter if the length of the query is at least 2 characters.
if (query.length >= 2) {
$rows.each(showViewRow);
}
else {
$rows.show();
}
}
if ($table.length) {
$rows = $table.find('tbody tr');
$input.on('keyup', filterViewList);
}
}
};
}(jQuery, Drupal));

View file

@ -0,0 +1,41 @@
<?php
namespace Drupal\views_ui\Ajax;
use Drupal\Core\Ajax\CommandInterface;
/**
* Provides an AJAX command for setting a form submit URL in modal forms.
*
* This command is implemented in Drupal.AjaxCommands.prototype.viewsSetForm.
*/
class SetFormCommand implements CommandInterface {
/**
* The URL of the form.
*
* @var string
*/
protected $url;
/**
* Constructs a SetFormCommand object.
*
* @param string $url
* The URL of the form.
*/
public function __construct($url) {
$this->url = $url;
}
/**
* {@inheritdoc}
*/
public function render() {
return array(
'command' => 'viewsSetForm',
'url' => $this->url,
);
}
}

View file

@ -0,0 +1,231 @@
<?php
namespace Drupal\views_ui\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Url;
use Drupal\views\ViewExecutable;
use Drupal\views\ViewEntityInterface;
use Drupal\views\Views;
use Drupal\views_ui\ViewUI;
use Drupal\views\ViewsData;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\ReplaceCommand;
use Drupal\Component\Utility\Html;
/**
* Returns responses for Views UI routes.
*/
class ViewsUIController extends ControllerBase {
/**
* Stores the Views data cache object.
*
* @var \Drupal\views\ViewsData
*/
protected $viewsData;
/**
* Constructs a new \Drupal\views_ui\Controller\ViewsUIController object.
*
* @param \Drupal\views\ViewsData $views_data
* The Views data cache object.
*/
public function __construct(ViewsData $views_data) {
$this->viewsData = $views_data;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('views.views_data')
);
}
/**
* Lists all instances of fields on any views.
*
* @return array
* The Views fields report page.
*/
public function reportFields() {
$views = $this->entityManager()->getStorage('view')->loadMultiple();
// Fetch all fieldapi fields which are used in views
// Therefore search in all views, displays and handler-types.
$fields = array();
$handler_types = ViewExecutable::getHandlerTypes();
foreach ($views as $view) {
$executable = $view->getExecutable();
$executable->initDisplay();
foreach ($executable->displayHandlers as $display_id => $display) {
if ($executable->setDisplay($display_id)) {
foreach ($handler_types as $type => $info) {
foreach ($executable->getHandlers($type, $display_id) as $item) {
$table_data = $this->viewsData->get($item['table']);
if (isset($table_data[$item['field']]) && isset($table_data[$item['field']][$type])
&& $field_data = $table_data[$item['field']][$type]) {
// The final check that we have a fieldapi field now.
if (isset($field_data['field_name'])) {
$fields[$field_data['field_name']][$view->id()] = $view->id();
}
}
}
}
}
}
}
$header = array(t('Field name'), t('Used in'));
$rows = array();
foreach ($fields as $field_name => $views) {
$rows[$field_name]['data'][0]['data']['#plain_text'] = $field_name;
foreach ($views as $view) {
$rows[$field_name]['data'][1][] = $this->l($view, new Url('entity.view.edit_form', array('view' => $view)));
}
$item_list = [
'#theme' => 'item_list',
'#items' => $rows[$field_name]['data'][1],
'#context' => ['list_style' => 'comma-list'],
];
$rows[$field_name]['data'][1] = ['data' => $item_list];
}
// Sort rows by field name.
ksort($rows);
$output = array(
'#type' => 'table',
'#header' => $header,
'#rows' => $rows,
'#empty' => t('No fields have been used in views yet.'),
);
return $output;
}
/**
* Lists all plugins and what enabled Views use them.
*
* @return array
* The Views plugins report page.
*/
public function reportPlugins() {
$rows = Views::pluginList();
foreach ($rows as &$row) {
$views = [];
// Link each view name to the view itself.
foreach ($row['views'] as $row_name => $view) {
$views[] = $this->l($view, new Url('entity.view.edit_form', array('view' => $view)));
}
unset($row['views']);
$row['views']['data'] = [
'#theme' => 'item_list',
'#items' => $views,
'#context' => ['list_style' => 'comma-list'],
];
}
// Sort rows by field name.
ksort($rows);
return array(
'#type' => 'table',
'#header' => array(t('Type'), t('Name'), t('Provided by'), t('Used in')),
'#rows' => $rows,
'#empty' => t('There are no enabled views.'),
);
}
/**
* Calls a method on a view and reloads the listing page.
*
* @param \Drupal\views\ViewEntityInterface $view
* The view being acted upon.
* @param string $op
* The operation to perform, e.g., 'enable' or 'disable'.
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request.
*
* @return \Drupal\Core\Ajax\AjaxResponse|\Symfony\Component\HttpFoundation\RedirectResponse
* Either returns a rebuilt listing page as an AJAX response, or redirects
* back to the listing page.
*/
public function ajaxOperation(ViewEntityInterface $view, $op, Request $request) {
// Perform the operation.
$view->$op()->save();
// If the request is via AJAX, return the rendered list as JSON.
if ($request->request->get('js')) {
$list = $this->entityManager()->getListBuilder('view')->render();
$response = new AjaxResponse();
$response->addCommand(new ReplaceCommand('#views-entity-list', $list));
return $response;
}
// Otherwise, redirect back to the page.
return $this->redirect('entity.view.collection');
}
/**
* Menu callback for Views tag autocompletion.
*
* Like other autocomplete functions, this function inspects the 'q' query
* parameter for the string to use to search for suggestions.
*
* @return \Symfony\Component\HttpFoundation\JsonResponse
* A JSON response containing the autocomplete suggestions for Views tags.
*/
public function autocompleteTag(Request $request) {
$matches = array();
$string = $request->query->get('q');
// Get matches from default views.
$views = $this->entityManager()->getStorage('view')->loadMultiple();
// Keep track of previously processed tags so they can be skipped.
$tags = [];
foreach ($views as $view) {
$tag = $view->get('tag');
if ($tag && !in_array($tag, $tags)) {
$tags[] = $tag;
if (strpos($tag, $string) === 0) {
$matches[] = ['value' => $tag, 'label' => Html::escape($tag)];
if (count($matches) >= 10) {
break;
}
}
}
}
return new JsonResponse($matches);
}
/**
* Returns the form to edit a view.
*
* @param \Drupal\views_ui\ViewUI $view
* The view to be edited.
* @param string|null $display_id
* (optional) The display ID being edited. Defaults to NULL, which will load
* the first available display.
*
* @return array
* An array containing the Views edit and preview forms.
*/
public function edit(ViewUI $view, $display_id = NULL) {
$name = $view->label();
$data = $this->viewsData->get($view->get('base_table'));
if (isset($data['table']['base']['title'])) {
$name .= ' (' . $data['table']['base']['title'] . ')';
}
$build['#title'] = $name;
$build['edit'] = $this->entityFormBuilder()->getForm($view, 'edit', array('display_id' => $display_id));
$build['preview'] = $this->entityFormBuilder()->getForm($view, 'preview', array('display_id' => $display_id));
return $build;
}
}

View file

@ -0,0 +1,107 @@
<?php
namespace Drupal\views_ui\Form;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\views\Views;
/**
* Form builder for the advanced admin settings page.
*/
class AdvancedSettingsForm extends ConfigFormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'views_ui_admin_settings_advanced';
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return ['views.settings'];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form = parent::buildForm($form, $form_state);
$config = $this->config('views.settings');
$form['cache'] = array(
'#type' => 'details',
'#title' => $this->t('Caching'),
'#open' => TRUE,
);
$form['cache']['skip_cache'] = array(
'#type' => 'checkbox',
'#title' => $this->t('Disable views data caching'),
'#description' => $this->t("Views caches data about tables, modules and views available, to increase performance. By checking this box, Views will skip this cache and always rebuild this data when needed. This can have a serious performance impact on your site."),
'#default_value' => $config->get('skip_cache'),
);
$form['cache']['clear_cache'] = array(
'#type' => 'submit',
'#value' => $this->t("Clear Views' cache"),
'#submit' => array('::cacheSubmit'),
);
$form['debug'] = array(
'#type' => 'details',
'#title' => $this->t('Debugging'),
'#open' => TRUE,
);
$form['debug']['sql_signature'] = array(
'#type' => 'checkbox',
'#title' => $this->t('Add Views signature to all SQL queries'),
'#description' => $this->t("All Views-generated queries will include the name of the views and display 'view-name:display-name' as a string at the end of the SELECT clause. This makes identifying Views queries in database server logs simpler, but should only be used when troubleshooting."),
'#default_value' => $config->get('sql_signature'),
);
$options = Views::fetchPluginNames('display_extender');
if (!empty($options)) {
$form['extenders'] = array(
'#type' => 'details',
'#open' => TRUE,
);
$form['extenders']['display_extenders'] = array(
'#title' => $this->t('Display extenders'),
'#default_value' => array_filter($config->get('display_extenders')),
'#options' => $options,
'#type' => 'checkboxes',
'#description' => $this->t('Select extensions of the views interface.')
);
}
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->config('views.settings')
->set('skip_cache', $form_state->getValue('skip_cache'))
->set('sql_signature', $form_state->getValue('sql_signature'))
->set('display_extenders', $form_state->getValue('display_extenders', array()))
->save();
parent::submitForm($form, $form_state);
}
/**
* Submission handler to clear the Views cache.
*/
public function cacheSubmit() {
views_invalidate_cache();
drupal_set_message($this->t('The cache has been cleared.'));
}
}

View file

@ -0,0 +1,187 @@
<?php
namespace Drupal\views_ui\Form\Ajax;
use Drupal\Core\Form\FormStateInterface;
use Drupal\views\ViewExecutable;
use Drupal\views\ViewEntityInterface;
use Drupal\views\Views;
/**
* Provides a form for adding an item in the Views UI.
*/
class AddHandler extends ViewsFormBase {
/**
* Constructs a new AddHandler object.
*/
public function __construct($type = NULL) {
$this->setType($type);
}
/**
* {@inheritdoc}
*/
public function getFormKey() {
return 'add-handler';
}
/**
* {@inheritdoc}
*/
public function getForm(ViewEntityInterface $view, $display_id, $js, $type = NULL) {
$this->setType($type);
return parent::getForm($view, $display_id, $js);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'views_ui_add_handler_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$view = $form_state->get('view');
$display_id = $form_state->get('display_id');
$type = $form_state->get('type');
$form = array(
'options' => array(
'#theme_wrappers' => array('container'),
'#attributes' => array('class' => array('scroll'), 'data-drupal-views-scroll' => TRUE),
),
);
$executable = $view->getExecutable();
if (!$executable->setDisplay($display_id)) {
$form['markup'] = array('#markup' => $this->t('Invalid display id @display', array('@display' => $display_id)));
return $form;
}
$display = &$executable->displayHandlers->get($display_id);
$types = ViewExecutable::getHandlerTypes();
$ltitle = $types[$type]['ltitle'];
$section = $types[$type]['plural'];
if (!empty($types[$type]['type'])) {
$type = $types[$type]['type'];
}
$form['#title'] = $this->t('Add @type', array('@type' => $ltitle));
$form['#section'] = $display_id . 'add-handler';
// Add the display override dropdown.
views_ui_standard_display_dropdown($form, $form_state, $section);
// Figure out all the base tables allowed based upon what the relationships provide.
$base_tables = $executable->getBaseTables();
$options = Views::viewsDataHelper()->fetchFields(array_keys($base_tables), $type, $display->useGroupBy(), $form_state->get('type'));
if (!empty($options)) {
$form['override']['controls'] = array(
'#theme_wrappers' => array('container'),
'#id' => 'views-filterable-options-controls',
'#attributes' => ['class' => ['form--inline', 'views-filterable-options-controls']],
);
$form['override']['controls']['options_search'] = array(
'#type' => 'textfield',
'#title' => $this->t('Search'),
);
$groups = array('all' => $this->t('- All -'));
$form['override']['controls']['group'] = array(
'#type' => 'select',
'#title' => $this->t('Category'),
'#options' => array(),
);
$form['options']['name'] = array(
'#prefix' => '<div class="views-radio-box form-checkboxes views-filterable-options">',
'#suffix' => '</div>',
'#type' => 'tableselect',
'#header' => array(
'title' => $this->t('Title'),
'group' => $this->t('Category'),
'help' => $this->t('Description'),
),
'#js_select' => FALSE,
);
$grouped_options = array();
foreach ($options as $key => $option) {
$group = preg_replace('/[^a-z0-9]/', '-', strtolower($option['group']));
$groups[$group] = $option['group'];
$grouped_options[$group][$key] = $option;
if (!empty($option['aliases']) && is_array($option['aliases'])) {
foreach ($option['aliases'] as $id => $alias) {
if (empty($alias['base']) || !empty($base_tables[$alias['base']])) {
$copy = $option;
$copy['group'] = $alias['group'];
$copy['title'] = $alias['title'];
if (isset($alias['help'])) {
$copy['help'] = $alias['help'];
}
$group = preg_replace('/[^a-z0-9]/', '-', strtolower($copy['group']));
$groups[$group] = $copy['group'];
$grouped_options[$group][$key . '$' . $id] = $copy;
}
}
}
}
foreach ($grouped_options as $group => $group_options) {
foreach ($group_options as $key => $option) {
$form['options']['name']['#options'][$key] = array(
'#attributes' => array(
'class' => array('filterable-option', $group),
),
'title' => array(
'data' => array(
'#title' => $option['title'],
'#plain_text' => $option['title'],
),
'class' => array('title'),
),
'group' => $option['group'],
'help' => array(
'data' => $option['help'],
'class' => array('description'),
),
);
}
}
$form['override']['controls']['group']['#options'] = $groups;
}
else {
$form['options']['markup'] = array(
'#markup' => '<div class="js-form-item form-item">' . $this->t('There are no @types available to add.', array('@types' => $ltitle)) . '</div>',
);
}
// Add a div to show the selected items
$form['selected'] = array(
'#type' => 'item',
'#markup' => '<span class="views-ui-view-title">' . $this->t('Selected:') . '</span> ' . '<div class="views-selected-options"></div>',
'#theme_wrappers' => array('form_element', 'views_ui_container'),
'#attributes' => array(
'class' => array('container-inline', 'views-add-form-selected', 'views-offset-bottom'),
'data-drupal-views-offset' => 'bottom',
),
);
$view->getStandardButtons($form, $form_state, 'views_ui_add_handler_form', $this->t('Add and configure @types', array('@types' => $ltitle)));
// Remove the default submit function.
$form['actions']['submit']['#submit'] = array_filter($form['actions']['submit']['#submit'], function($var) {
return !(is_array($var) && isset($var[1]) && $var[1] == 'standardSubmit');
});
$form['actions']['submit']['#submit'][] = array($view, 'submitItemAdd');
return $form;
}
}

View file

@ -0,0 +1,60 @@
<?php
namespace Drupal\views_ui\Form\Ajax;
use Drupal\Core\Form\FormStateInterface;
use Drupal\views\Views;
/**
* Displays analysis information for a view.
*/
class Analyze extends ViewsFormBase {
/**
* {@inheritdoc}
*/
public function getFormKey() {
return 'analyze';
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'views_ui_analyze_view_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$view = $form_state->get('view');
$form['#title'] = $this->t('View analysis');
$form['#section'] = 'analyze';
$analyzer = Views::analyzer();
$messages = $analyzer->getMessages($view->getExecutable());
$form['analysis'] = array(
'#prefix' => '<div class="js-form-item form-item">',
'#suffix' => '</div>',
'#markup' => $analyzer->formatMessages($messages),
);
// Inform the standard button function that we want an OK button.
$form_state->set('ok_button', TRUE);
$view->getStandardButtons($form, $form_state, 'views_ui_analyze_view_form');
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
/** @var $view \Drupal\views_ui\ViewUI */
$view = $form_state->get('view');
$form_state->setRedirectUrl($view->urlInfo('edit-form'));
}
}

View file

@ -0,0 +1,280 @@
<?php
namespace Drupal\views_ui\Form\Ajax;
use Drupal\Core\Form\FormStateInterface;
use Drupal\views\ViewEntityInterface;
use Drupal\views\ViewExecutable;
use Drupal\views\Views;
use Symfony\Component\HttpFoundation\Request;
/**
* Provides a form for configuring an item in the Views UI.
*/
class ConfigHandler extends ViewsFormBase {
/**
* Constructs a new ConfigHandler object.
*/
public function __construct($type = NULL, $id = NULL) {
$this->setType($type);
$this->setID($id);
}
/**
* {@inheritdoc}
*/
public function getFormKey() {
return 'handler';
}
/**
* {@inheritdoc}
*/
public function getForm(ViewEntityInterface $view, $display_id, $js, $type = NULL, $id = NULL) {
$this->setType($type);
$this->setID($id);
return parent::getForm($view, $display_id, $js);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'views_ui_config_item_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, Request $request = NULL) {
/** @var \Drupal\views\Entity\View $view */
$view = $form_state->get('view');
$display_id = $form_state->get('display_id');
$type = $form_state->get('type');
$id = $form_state->get('id');
$form = array(
'options' => array(
'#tree' => TRUE,
'#theme_wrappers' => array('container'),
'#attributes' => array('class' => array('scroll'), 'data-drupal-views-scroll' => TRUE),
),
);
$executable = $view->getExecutable();
$save_ui_cache = FALSE;
if (!$executable->setDisplay($display_id)) {
$form['markup'] = array('#markup' => $this->t('Invalid display id @display', array('@display' => $display_id)));
return $form;
}
$item = $executable->getHandler($display_id, $type, $id);
if ($item) {
$handler = $executable->display_handler->getHandler($type, $id);
if (empty($handler)) {
$form['markup'] = array('#markup' => $this->t("Error: handler for @table > @field doesn't exist!", array('@table' => $item['table'], '@field' => $item['field'])));
}
else {
$types = ViewExecutable::getHandlerTypes();
// If this item can come from the default display, show a dropdown
// that lets the user choose which display the changes should apply to.
if ($executable->display_handler->defaultableSections($types[$type]['plural'])) {
$section = $types[$type]['plural'];
$form_state->set('section', $section);
views_ui_standard_display_dropdown($form, $form_state, $section);
}
// A whole bunch of code to figure out what relationships are valid for
// this item.
$relationships = $executable->display_handler->getOption('relationships');
$relationship_options = array();
foreach ($relationships as $relationship) {
// relationships can't link back to self. But also, due to ordering,
// relationships can only link to prior relationships.
if ($type == 'relationship' && $id == $relationship['id']) {
break;
}
$relationship_handler = Views::handlerManager('relationship')->getHandler($relationship);
// ignore invalid/broken relationships.
if (empty($relationship_handler)) {
continue;
}
// If this relationship is valid for this type, add it to the list.
$data = Views::viewsData()->get($relationship['table']);
if (isset($data[$relationship['field']]['relationship']['base']) && $base = $data[$relationship['field']]['relationship']['base']) {
$base_fields = Views::viewsDataHelper()->fetchFields($base, $type, $executable->display_handler->useGroupBy());
if (isset($base_fields[$item['table'] . '.' . $item['field']])) {
$relationship_handler->init($executable, $executable->display_handler, $relationship);
$relationship_options[$relationship['id']] = $relationship_handler->adminLabel();
}
}
}
if (!empty($relationship_options)) {
// Make sure the existing relationship is even valid. If not, force
// it to none.
$base_fields = Views::viewsDataHelper()->fetchFields($view->get('base_table'), $type, $executable->display_handler->useGroupBy());
if (isset($base_fields[$item['table'] . '.' . $item['field']])) {
$relationship_options = array_merge(array('none' => $this->t('Do not use a relationship')), $relationship_options);
}
$rel = empty($item['relationship']) ? 'none' : $item['relationship'];
if (empty($relationship_options[$rel])) {
// Pick the first relationship.
$rel = key($relationship_options);
// We want this relationship option to get saved even if the user
// skips submitting the form.
$executable->setHandlerOption($display_id, $type, $id, 'relationship', $rel);
$save_ui_cache = TRUE;
// Re-initialize with new relationship.
$item['relationship'] = $rel;
$handler->init($executable, $executable->display_handler, $item);
}
$form['options']['relationship'] = array(
'#type' => 'select',
'#title' => $this->t('Relationship'),
'#options' => $relationship_options,
'#default_value' => $rel,
'#weight' => -500,
);
}
else {
$form['options']['relationship'] = array(
'#type' => 'value',
'#value' => 'none',
);
}
$form['#title'] = $this->t('Configure @type: @item', array('@type' => $types[$type]['lstitle'], '@item' => $handler->adminLabel()));
if (!empty($handler->definition['help'])) {
$form['options']['form_description'] = array(
'#markup' => $handler->definition['help'],
'#theme_wrappers' => array('container'),
'#attributes' => array('class' => array('js-form-item form-item description')),
'#weight' => -1000,
);
}
$form['#section'] = $display_id . '-' . $type . '-' . $id;
// Get form from the handler.
$handler->buildOptionsForm($form['options'], $form_state);
$form_state->set('handler', $handler);
}
$name = $form_state->get('update_name');
$view->getStandardButtons($form, $form_state, 'views_ui_config_item_form', $name);
// Add a 'remove' button.
$form['actions']['remove'] = array(
'#type' => 'submit',
'#value' => $this->t('Remove'),
'#submit' => array(array($this, 'remove')),
'#limit_validation_errors' => array(array('override')),
'#button_type' => 'danger',
);
}
if ($save_ui_cache) {
$view->cacheSet();
}
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
$form_state->get('handler')->validateOptionsForm($form['options'], $form_state);
if ($form_state->getErrors()) {
$form_state->set('rerender', TRUE);
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$view = $form_state->get('view');
$display_id = $form_state->get('display_id');
$id = $form_state->get('id');
$handler = $form_state->get('handler');
// Run it through the handler's submit function.
$handler->submitOptionsForm($form['options'], $form_state);
$item = $handler->options;
$types = ViewExecutable::getHandlerTypes();
// For footer/header $handler_type is area but $type is footer/header.
// For all other handle types it's the same.
$handler_type = $type = $form_state->get('type');
if (!empty($types[$type]['type'])) {
$handler_type = $types[$type]['type'];
}
$override = NULL;
$executable = $view->getExecutable();
if ($executable->display_handler->useGroupBy() && !empty($item['group_type'])) {
if (empty($executable->query)) {
$executable->initQuery();
}
$aggregate = $executable->query->getAggregationInfo();
if (!empty($aggregate[$item['group_type']]['handler'][$type])) {
$override = $aggregate[$item['group_type']]['handler'][$type];
}
}
// Create a new handler and unpack the options from the form onto it. We
// can use that for storage.
$handler = Views::handlerManager($handler_type)->getHandler($item, $override);
$handler->init($executable, $executable->display_handler, $item);
// Add the incoming options to existing options because items using
// the extra form may not have everything in the form here.
$options = $handler->submitFormCalculateOptions($handler->options, $form_state->getValue('options', []));
// This unpacks only options that are in the definition, ensuring random
// extra stuff on the form is not sent through.
$handler->unpackOptions($handler->options, $options, NULL, FALSE);
// Store the item back on the view
$executable->setHandler($display_id, $type, $id, $handler->options);
// Ensure any temporary options are removed.
if (isset($view->temporary_options[$type][$id])) {
unset($view->temporary_options[$type][$id]);
}
// Write to cache
$view->cacheSet();
}
/**
* Submit handler for removing an item from a view
*/
public function remove(&$form, FormStateInterface $form_state) {
$view = $form_state->get('view');
$display_id = $form_state->get('display_id');
$type = $form_state->get('type');
$id = $form_state->get('id');
// Store the item back on the view
list($was_defaulted, $is_defaulted) = $view->getOverrideValues($form, $form_state);
$executable = $view->getExecutable();
// If the display selection was changed toggle the override value.
if ($was_defaulted != $is_defaulted) {
$display = &$executable->displayHandlers->get($display_id);
$display->optionsOverride($form, $form_state);
}
$executable->removeHandler($display_id, $type, $id);
// Write to cache
$view->cacheSet();
}
}

View file

@ -0,0 +1,120 @@
<?php
namespace Drupal\views_ui\Form\Ajax;
use Drupal\Core\Form\FormStateInterface;
use Drupal\views\ViewEntityInterface;
use Drupal\views\ViewExecutable;
/**
* Provides a form for configuring extra information for a Views UI item.
*/
class ConfigHandlerExtra extends ViewsFormBase {
/**
* Constructs a new ConfigHandlerExtra object.
*/
public function __construct($type = NULL, $id = NULL) {
$this->setType($type);
$this->setID($id);
}
/**
* {@inheritdoc}
*/
public function getFormKey() {
return 'handler-extra';
}
/**
* {@inheritdoc}
*/
public function getForm(ViewEntityInterface $view, $display_id, $js, $type = NULL, $id = NULL) {
$this->setType($type);
$this->setID($id);
return parent::getForm($view, $display_id, $js);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'views_ui_config_item_extra_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$view = $form_state->get('view');
$display_id = $form_state->get('display_id');
$type = $form_state->get('type');
$id = $form_state->get('id');
$form = array(
'options' => array(
'#tree' => TRUE,
'#theme_wrappers' => array('container'),
'#attributes' => array('class' => array('scroll'), 'data-drupal-views-scroll' => TRUE),
),
);
$executable = $view->getExecutable();
if (!$executable->setDisplay($display_id)) {
$form['markup'] = array('#markup' => $this->t('Invalid display id @display', array('@display' => $display_id)));
return $form;
}
$item = $executable->getHandler($display_id, $type, $id);
if ($item) {
$handler = $executable->display_handler->getHandler($type, $id);
if (empty($handler)) {
$form['markup'] = array('#markup' => $this->t("Error: handler for @table > @field doesn't exist!", array('@table' => $item['table'], '@field' => $item['field'])));
}
else {
$handler->init($executable, $executable->display_handler, $item);
$types = ViewExecutable::getHandlerTypes();
$form['#title'] = $this->t('Configure extra settings for @type %item', array('@type' => $types[$type]['lstitle'], '%item' => $handler->adminLabel()));
$form['#section'] = $display_id . '-' . $type . '-' . $id;
// Get form from the handler.
$handler->buildExtraOptionsForm($form['options'], $form_state);
$form_state->set('handler', $handler);
}
$view->getStandardButtons($form, $form_state, 'views_ui_config_item_extra_form');
}
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
$form_state->get('handler')->validateExtraOptionsForm($form['options'], $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$view = $form_state->get('view');
$handler = $form_state->get('handler');
// Run it through the handler's submit function.
$handler->submitExtraOptionsForm($form['options'], $form_state);
$item = $handler->options;
// Store the data we're given.
foreach ($form_state->getValue('options') as $key => $value) {
$item[$key] = $value;
}
// Store the item back on the view
$view->getExecutable()->setHandler($form_state->get('display_id'), $form_state->get('type'), $form_state->get('id'), $item);
// Write to cache
$view->cacheSet();
}
}

View file

@ -0,0 +1,113 @@
<?php
namespace Drupal\views_ui\Form\Ajax;
use Drupal\Core\Form\FormStateInterface;
use Drupal\views\Views;
use Drupal\views\ViewEntityInterface;
use Drupal\views\ViewExecutable;
/**
* Provides a form for configuring grouping information for a Views UI handler.
*/
class ConfigHandlerGroup extends ViewsFormBase {
/**
* Constructs a new ConfigHandlerGroup object.
*/
public function __construct($type = NULL, $id = NULL) {
$this->setType($type);
$this->setID($id);
}
/**
* {@inheritdoc}
*/
public function getFormKey() {
return 'handler-group';
}
/**
* {@inheritdoc}
*/
public function getForm(ViewEntityInterface $view, $display_id, $js, $type = NULL, $id = NULL) {
$this->setType($type);
$this->setID($id);
return parent::getForm($view, $display_id, $js);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'views_ui_config_item_group_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$view = $form_state->get('view');
$display_id = $form_state->get('display_id');
$type = $form_state->get('type');
$id = $form_state->get('id');
$form = array(
'options' => array(
'#tree' => TRUE,
'#theme_wrappers' => array('container'),
'#attributes' => array('class' => array('scroll'), 'data-drupal-views-scroll' => TRUE),
),
);
$executable = $view->getExecutable();
if (!$executable->setDisplay($display_id)) {
$form['markup'] = array('#markup' => $this->t('Invalid display id @display', array('@display' => $display_id)));
return $form;
}
$executable->initQuery();
$item = $executable->getHandler($display_id, $type, $id);
if ($item) {
$handler = $executable->display_handler->getHandler($type, $id);
if (empty($handler)) {
$form['markup'] = array('#markup' => $this->t("Error: handler for @table > @field doesn't exist!", array('@table' => $item['table'], '@field' => $item['field'])));
}
else {
$handler->init($executable, $executable->display_handler, $item);
$types = ViewExecutable::getHandlerTypes();
$form['#title'] = $this->t('Configure aggregation settings for @type %item', array('@type' => $types[$type]['lstitle'], '%item' => $handler->adminLabel()));
$handler->buildGroupByForm($form['options'], $form_state);
$form_state->set('handler', $handler);
}
$view->getStandardButtons($form, $form_state, 'views_ui_config_item_group_form');
}
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$view = $form_state->get('view');
$item = &$form_state->get('handler')->options;
$type = $form_state->get('type');
$handler = Views::handlerManager($type)->getHandler($item);
$executable = $view->getExecutable();
$handler->init($executable, $executable->display_handler, $item);
$handler->submitGroupByForm($form, $form_state);
// Store the item back on the view
$executable->setHandler($form_state->get('display_id'), $form_state->get('type'), $form_state->get('id'), $item);
// Write to cache
$view->cacheSet();
}
}

View file

@ -0,0 +1,114 @@
<?php
namespace Drupal\views_ui\Form\Ajax;
use Drupal\Core\Form\FormStateInterface;
use Drupal\views\ViewEntityInterface;
/**
* Provides a form for editing the Views display.
*/
class Display extends ViewsFormBase {
/**
* Constructs a new Display object.
*/
public function __construct($type = NULL) {
$this->setType($type);
}
/**
* {@inheritdoc}
*/
public function getFormKey() {
return 'display';
}
/**
* {@inheritdoc}
*
* @todo Remove this and switch all usage of $form_state->get('section') to
* $form_state->get('type').
*/
public function getFormState(ViewEntityInterface $view, $display_id, $js) {
$form_state = parent::getFormState($view, $display_id, $js);
$form_state->set('section', $this->type);
return $form_state;
}
/**
* {@inheritdoc}
*/
public function getForm(ViewEntityInterface $view, $display_id, $js, $type = NULL) {
$this->setType($type);
return parent::getForm($view, $display_id, $js);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'views_ui_edit_display_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$view = $form_state->get('view');
$display_id = $form_state->get('display_id');
$executable = $view->getExecutable();
if (!$executable->setDisplay($display_id)) {
$form['markup'] = array('#markup' => $this->t('Invalid display id @display', array('@display' => $display_id)));
return $form;
}
// Get form from the handler.
$form['options'] = array(
'#theme_wrappers' => array('container'),
'#attributes' => array('class' => array('scroll'), 'data-drupal-views-scroll' => TRUE),
);
$executable->display_handler->buildOptionsForm($form['options'], $form_state);
// The handler options form sets $form['#title'], which we need on the entire
// $form instead of just the ['options'] section.
$form['#title'] = $form['options']['#title'];
unset($form['options']['#title']);
// Move the override dropdown out of the scrollable section of the form.
if (isset($form['options']['override'])) {
$form['override'] = $form['options']['override'];
unset($form['options']['override']);
}
$name = $form_state->get('update_name');
$view->getStandardButtons($form, $form_state, 'views_ui_edit_display_form', $name);
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
$view = $form_state->get('view');
$display_id = $form_state->get('display_id');
$view->getExecutable()->displayHandlers->get($display_id)->validateOptionsForm($form['options'], $form_state);
if ($form_state->getErrors()) {
$form_state->set('rerender', TRUE);
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$view = $form_state->get('view');
$display_id = $form_state->get('display_id');
$view->getExecutable()->displayHandlers->get($display_id)->submitOptionsForm($form['options'], $form_state);
$view->cacheSet();
}
}

View file

@ -0,0 +1,90 @@
<?php
namespace Drupal\views_ui\Form\Ajax;
use Drupal\Core\Form\FormStateInterface;
use Drupal\views\Views;
/**
* Provides a form for editing the details of a View.
*/
class EditDetails extends ViewsFormBase {
/**
* {@inheritdoc}
*/
public function getFormKey() {
return 'edit-details';
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'views_ui_edit_details_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$view = $form_state->get('view');
$form['#title'] = $this->t('Name and description');
$form['#section'] = 'details';
$form['details'] = array(
'#theme_wrappers' => array('container'),
'#attributes' => array('class' => array('scroll'), 'data-drupal-views-scroll' => TRUE),
);
$form['details']['label'] = array(
'#type' => 'textfield',
'#title' => t('Administrative name'),
'#default_value' => $view->label(),
);
$form['details']['langcode'] = array(
'#type' => 'language_select',
'#title' => $this->t('View language'),
'#description' => $this->t('Language of labels and other textual elements in this view.'),
'#default_value' => $view->get('langcode'),
);
$form['details']['description'] = array(
'#type' => 'textfield',
'#title' => t('Administrative description'),
'#default_value' => $view->get('description'),
);
$form['details']['tag'] = array(
'#type' => 'textfield',
'#title' => t('Administrative tags'),
'#description' => t('Enter a comma-separated list of words to describe your view.'),
'#default_value' => $view->get('tag'),
'#autocomplete_route_name' => 'views_ui.autocomplete',
);
$view->getStandardButtons($form, $form_state, 'views_ui_edit_details_form');
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$view = $form_state->get('view');
foreach ($form_state->getValues() as $key => $value) {
// Only save values onto the view if they're actual view properties
// (as opposed to 'op' or 'form_build_id').
if (isset($form['details'][$key])) {
$view->set($key, $value);
}
}
$bases = Views::viewsData()->fetchBaseTables();
$page_title = $view->label();
if (isset($bases[$view->get('base_table')])) {
$page_title .= ' (' . $bases[$view->get('base_table')]['title'] . ')';
}
$form_state->set('page_title', $page_title);
$view->cacheSet();
}
}

View file

@ -0,0 +1,179 @@
<?php
namespace Drupal\views_ui\Form\Ajax;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Drupal\views\ViewEntityInterface;
use Drupal\views\ViewExecutable;
/**
* Provides a rearrange form for Views handlers.
*/
class Rearrange extends ViewsFormBase {
/**
* Constructs a new Rearrange object.
*/
public function __construct($type = NULL) {
$this->setType($type);
}
/**
* {@inheritdoc}
*/
public function getFormKey() {
return 'rearrange';
}
/**
* {@inheritdoc}
*/
public function getForm(ViewEntityInterface $view, $display_id, $js, $type = NULL) {
$this->setType($type);
return parent::getForm($view, $display_id, $js);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'views_ui_rearrange_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$view = $form_state->get('view');
$display_id = $form_state->get('display_id');
$type = $form_state->get('type');
$types = ViewExecutable::getHandlerTypes();
$executable = $view->getExecutable();
if (!$executable->setDisplay($display_id)) {
$form['markup'] = array('#markup' => $this->t('Invalid display id @display', array('@display' => $display_id)));
return $form;
}
$display = &$executable->displayHandlers->get($display_id);
$form['#title'] = $this->t('Rearrange @type', array('@type' => $types[$type]['ltitle']));
$form['#section'] = $display_id . 'rearrange-item';
if ($display->defaultableSections($types[$type]['plural'])) {
$section = $types[$type]['plural'];
$form_state->set('section', $section);
views_ui_standard_display_dropdown($form, $form_state, $section);
}
$count = 0;
// Get relationship labels
$relationships = array();
foreach ($display->getHandlers('relationship') as $id => $handler) {
$relationships[$id] = $handler->adminLabel();
}
$form['fields'] = array(
'#type' => 'table',
'#header' => array('', $this->t('Weight'), $this->t('Remove')),
'#empty' => $this->t('No fields available.'),
'#tabledrag' => array(
array(
'action' => 'order',
'relationship' => 'sibling',
'group' => 'weight',
)
),
'#tree' => TRUE,
'#prefix' => '<div class="scroll" data-drupal-views-scroll>',
'#suffix' => '</div>',
);
foreach ($display->getOption($types[$type]['plural']) as $id => $field) {
$form['fields'][$id] = array();
$form['fields'][$id]['#attributes'] = array('class' => array('draggable'), 'id' => 'views-row-' . $id);
$handler = $display->getHandler($type, $id);
if ($handler) {
$name = $handler->adminLabel() . ' ' . $handler->adminSummary();
if (!empty($field['relationship']) && !empty($relationships[$field['relationship']])) {
$name = '(' . $relationships[$field['relationship']] . ') ' . $name;
}
$markup = $name;
}
else {
$name = $id;
$markup = $this->t('Broken field @id', array('@id' => $id));
}
$form['fields'][$id]['name'] = array('#markup' => $markup);
$form['fields'][$id]['weight'] = array(
'#type' => 'textfield',
'#default_value' => ++$count,
'#attributes' => array('class' => array('weight')),
'#title' => t('Weight for @title', array('@title' => $name)),
'#title_display' => 'invisible',
);
$form['fields'][$id]['removed'] = array(
'#type' => 'checkbox',
'#title' => t('Remove @title', array('@title' => $name)),
'#title_display' => 'invisible',
'#id' => 'views-removed-' . $id,
'#attributes' => array('class' => array('views-remove-checkbox')),
'#default_value' => 0,
'#suffix' => \Drupal::l(SafeMarkup::format('<span>@text</span>', array('@text' => $this->t('Remove'))),
Url::fromRoute('<none>', array(), array('attributes' => array(
'id' => 'views-remove-link-' . $id,
'class' => array('views-hidden', 'views-button-remove', 'views-remove-link'),
'alt' => $this->t('Remove this item'),
'title' => $this->t('Remove this item')),
))
),
);
}
$view->getStandardButtons($form, $form_state, 'views_ui_rearrange_form');
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$view = $form_state->get('view');
$display_id = $form_state->get('display_id');
$type = $form_state->get('type');
$types = ViewExecutable::getHandlerTypes();
$display = &$view->getExecutable()->displayHandlers->get($display_id);
$old_fields = $display->getOption($types[$type]['plural']);
$new_fields = $order = array();
// Make an array with the weights
foreach ($form_state->getValue('fields') as $field => $info) {
// add each value that is a field with a weight to our list, but only if
// it has had its 'removed' checkbox checked.
if (is_array($info) && isset($info['weight']) && empty($info['removed'])) {
$order[$field] = $info['weight'];
}
}
// Sort the array
asort($order);
// Create a new list of fields in the new order.
foreach (array_keys($order) as $field) {
$new_fields[$field] = $old_fields[$field];
}
$display->setOption($types[$type]['plural'], $new_fields);
// Store in cache
$view->cacheSet();
}
}

View file

@ -0,0 +1,355 @@
<?php
namespace Drupal\views_ui\Form\Ajax;
use Drupal\Component\Utility\Html;
use Drupal\Core\Form\FormStateInterface;
use Drupal\views\ViewExecutable;
/**
* Provides a rearrange form for Views filters.
*/
class RearrangeFilter extends ViewsFormBase {
/**
* Constructs a new RearrangeFilter object.
*/
public function __construct($type = NULL) {
$this->setType($type);
}
/**
* {@inheritdoc}
*/
public function getFormKey() {
return 'rearrange-filter';
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'views_ui_rearrange_filter_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$view = $form_state->get('view');
$display_id = $form_state->get('display_id');
$type = 'filter';
$types = ViewExecutable::getHandlerTypes();
$executable = $view->getExecutable();
if (!$executable->setDisplay($display_id)) {
$form['markup'] = array('#markup' => $this->t('Invalid display id @display', array('@display' => $display_id)));
return $form;
}
$display = $executable->displayHandlers->get($display_id);
$form['#title'] = Html::escape($display->display['display_title']) . ': ';
$form['#title'] .= $this->t('Rearrange @type', array('@type' => $types[$type]['ltitle']));
$form['#section'] = $display_id . 'rearrange-item';
if ($display->defaultableSections($types[$type]['plural'])) {
$section = $types[$type]['plural'];
$form_state->set('section', $section);
views_ui_standard_display_dropdown($form, $form_state, $section);
}
if (!empty($view->form_cache)) {
$groups = $view->form_cache['groups'];
$handlers = $view->form_cache['handlers'];
}
else {
$groups = $display->getOption('filter_groups');
$handlers = $display->getOption($types[$type]['plural']);
}
$count = 0;
// Get relationship labels
$relationships = array();
foreach ($display->getHandlers('relationship') as $id => $handler) {
$relationships[$id] = $handler->adminLabel();
}
$group_options = array();
/**
* Filter groups is an array that contains:
* array(
* 'operator' => 'and' || 'or',
* 'groups' => array(
* $group_id => 'and' || 'or',
* ),
* );
*/
$grouping = count(array_keys($groups['groups'])) > 1;
$form['filter_groups']['#tree'] = TRUE;
$form['filter_groups']['operator'] = array(
'#type' => 'select',
'#options' => array(
'AND' => $this->t('And'),
'OR' => $this->t('Or'),
),
'#default_value' => $groups['operator'],
'#attributes' => array(
'class' => array('warning-on-change'),
),
'#title' => $this->t('Operator to use on all groups'),
'#description' => $this->t('Either "group 0 AND group 1 AND group 2" or "group 0 OR group 1 OR group 2", etc'),
'#access' => $grouping,
);
$form['remove_groups']['#tree'] = TRUE;
foreach ($groups['groups'] as $id => $group) {
$form['filter_groups']['groups'][$id] = array(
'#title' => $this->t('Operator'),
'#type' => 'select',
'#options' => array(
'AND' => $this->t('And'),
'OR' => $this->t('Or'),
),
'#default_value' => $group,
'#attributes' => array(
'class' => array('warning-on-change'),
),
);
$form['remove_groups'][$id] = array(); // to prevent a notice
if ($id != 1) {
$form['remove_groups'][$id] = array(
'#type' => 'submit',
'#value' => $this->t('Remove group @group', array('@group' => $id)),
'#id' => "views-remove-group-$id",
'#attributes' => array(
'class' => array('views-remove-group'),
),
'#group' => $id,
'#ajax' => ['url' => NULL],
);
}
$group_options[$id] = $id == 1 ? $this->t('Default group') : $this->t('Group @group', array('@group' => $id));
$form['#group_renders'][$id] = array();
}
$form['#group_options'] = $group_options;
$form['#groups'] = $groups;
// We don't use getHandlers() because we want items without handlers to
// appear and show up as 'broken' so that the user can see them.
$form['filters'] = array('#tree' => TRUE);
foreach ($handlers as $id => $field) {
// If the group does not exist, move the filters to the default group.
if (empty($field['group']) || empty($groups['groups'][$field['group']])) {
$field['group'] = 1;
}
$handler = $display->getHandler($type, $id);
if ($grouping && $handler && !$handler->canGroup()) {
$field['group'] = 'ungroupable';
}
// If not grouping and the handler is set ungroupable, move it back to
// the default group to prevent weird errors from having it be in its
// own group:
if (!$grouping && $field['group'] == 'ungroupable') {
$field['group'] = 1;
}
// Place this item into the proper group for rendering.
$form['#group_renders'][$field['group']][] = $id;
$form['filters'][$id]['weight'] = array(
'#title' => t('Weight for @id', array('@id' => $id)),
'#title_display' => 'invisible',
'#type' => 'textfield',
'#default_value' => ++$count,
'#size' => 8,
);
$form['filters'][$id]['group'] = array(
'#title' => t('Group for @id', array('@id' => $id)),
'#title_display' => 'invisible',
'#type' => 'select',
'#options' => $group_options,
'#default_value' => $field['group'],
'#attributes' => array(
'class' => array('views-region-select', 'views-region-' . $id),
),
'#access' => $field['group'] !== 'ungroupable',
);
if ($handler) {
$name = $handler->adminLabel() . ' ' . $handler->adminSummary();
if (!empty($field['relationship']) && !empty($relationships[$field['relationship']])) {
$name = '(' . $relationships[$field['relationship']] . ') ' . $name;
}
$form['filters'][$id]['name'] = array(
'#markup' => $name,
);
}
else {
$form['filters'][$id]['name'] = array('#markup' => $this->t('Broken field @id', array('@id' => $id)));
}
$form['filters'][$id]['removed'] = array(
'#title' => t('Remove @id', array('@id' => $id)),
'#title_display' => 'invisible',
'#type' => 'checkbox',
'#id' => 'views-removed-' . $id,
'#attributes' => array('class' => array('views-remove-checkbox')),
'#default_value' => 0,
);
}
$view->getStandardButtons($form, $form_state, 'views_ui_rearrange_filter_form');
$form['actions']['add_group'] = array(
'#type' => 'submit',
'#value' => $this->t('Create new filter group'),
'#id' => 'views-add-group',
'#group' => 'add',
'#attributes' => array(
'class' => array('views-add-group'),
),
'#ajax' => ['url' => NULL],
);
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$types = ViewExecutable::getHandlerTypes();
$view = $form_state->get('view');
$display = &$view->getExecutable()->displayHandlers->get($form_state->get('display_id'));
$remember_groups = array();
if (!empty($view->form_cache)) {
$old_fields = $view->form_cache['handlers'];
}
else {
$old_fields = $display->getOption($types['filter']['plural']);
}
$groups = $form_state->getValue('filter_groups');
// Whatever button was clicked, re-calculate field information.
$new_fields = $order = array();
// Make an array with the weights
foreach ($form_state->getValue('filters') as $field => $info) {
// add each value that is a field with a weight to our list, but only if
// it has had its 'removed' checkbox checked.
if (is_array($info) && empty($info['removed'])) {
if (isset($info['weight'])) {
$order[$field] = $info['weight'];
}
if (isset($info['group'])) {
$old_fields[$field]['group'] = $info['group'];
$remember_groups[$info['group']][] = $field;
}
}
}
// Sort the array
asort($order);
// Create a new list of fields in the new order.
foreach (array_keys($order) as $field) {
$new_fields[$field] = $old_fields[$field];
}
// If the #group property is set on the clicked button, that means we are
// either adding or removing a group, not actually updating the filters.
$triggering_element = $form_state->getTriggeringElement();
if (!empty($triggering_element['#group'])) {
if ($triggering_element['#group'] == 'add') {
// Add a new group
$groups['groups'][] = 'AND';
}
else {
// Renumber groups above the removed one down.
foreach (array_keys($groups['groups']) as $group_id) {
if ($group_id >= $triggering_element['#group']) {
$old_group = $group_id + 1;
if (isset($groups['groups'][$old_group])) {
$groups['groups'][$group_id] = $groups['groups'][$old_group];
if (isset($remember_groups[$old_group])) {
foreach ($remember_groups[$old_group] as $id) {
$new_fields[$id]['group'] = $group_id;
}
}
}
else {
// If this is the last one, just unset it.
unset($groups['groups'][$group_id]);
}
}
}
}
// Update our cache with values so that cancel still works the way
// people expect.
$view->form_cache = [
'key' => 'rearrange-filter',
'groups' => $groups,
'handlers' => $new_fields,
];
// Return to this form except on actual Update.
$view->addFormToStack('rearrange-filter', $form_state->get('display_id'), 'filter');
}
else {
// The actual update button was clicked. Remove the empty groups, and
// renumber them sequentially.
ksort($remember_groups);
$groups['groups'] = static::arrayKeyPlus(array_values(array_intersect_key($groups['groups'], $remember_groups)));
// Change the 'group' key on each field to match. Here, $mapping is an
// array whose keys are the old group numbers and whose values are the new
// (sequentially numbered) ones.
$mapping = array_flip(static::arrayKeyPlus(array_keys($remember_groups)));
foreach ($new_fields as &$new_field) {
$new_field['group'] = $mapping[$new_field['group']];
}
// Write the changed handler values.
$display->setOption($types['filter']['plural'], $new_fields);
$display->setOption('filter_groups', $groups);
if (isset($view->form_cache)) {
unset($view->form_cache);
}
}
// Store in cache.
$view->cacheSet();
}
/**
* Adds one to each key of an array.
*
* For example array(0 => 'foo') would be array(1 => 'foo').
*
* @param array $array
* The array to increment keys on.
*
* @return array
* The array with incremented keys.
*/
public static function arrayKeyPlus($array) {
$keys = array_keys($array);
// Sort the keys in reverse order so incrementing them doesn't overwrite any
// existing keys.
rsort($keys);
foreach ($keys as $key) {
$array[$key + 1] = $array[$key];
unset($array[$key]);
}
// Sort the keys back to ascending order.
ksort($array);
return $array;
}
}

View file

@ -0,0 +1,199 @@
<?php
namespace Drupal\views_ui\Form\Ajax;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
/**
* Displays the display reorder form.
*/
class ReorderDisplays extends ViewsFormBase {
/**
* {@inheritdoc}
*/
public function getFormKey() {
return 'reorder-displays';
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'views_ui_reorder_displays_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
/** @var $view \Drupal\views\ViewEntityInterface */
$view = $form_state->get('view');
$display_id = $form_state->get('display_id');
$form['#title'] = $this->t('Reorder displays');
$form['#section'] = 'reorder';
$form['#action'] = $this->url('views_ui.form_reorder_displays', [
'js' => 'nojs',
'view' => $view->id(),
'display_id' => $display_id,
]);
$form['view'] = array(
'#type' => 'value',
'#value' => $view
);
$displays = $view->get('display');
$count = count($displays);
// Sort the displays.
uasort($displays, function ($display1, $display2) {
if ($display1['position'] != $display2['position']) {
return $display1['position'] < $display2['position'] ? -1 : 1;
}
return 0;
});
$form['displays'] = array(
'#type' => 'table',
'#id' => 'reorder-displays',
'#header' => array($this->t('Display'), $this->t('Weight'), $this->t('Remove')),
'#empty' => $this->t('No displays available.'),
'#tabledrag' => array(
array(
'action' => 'order',
'relationship' => 'sibling',
'group' => 'weight',
)
),
'#tree' => TRUE,
'#prefix' => '<div class="scroll" data-drupal-views-scroll>',
'#suffix' => '</div>',
);
foreach ($displays as $id => $display) {
$form['displays'][$id] = array(
'#display' => $display,
'#attributes' => array(
'id' => 'display-row-' . $id,
),
'#weight' => $display['position'],
);
// Only make row draggable if it's not the default display.
if ($id !== 'default') {
$form['displays'][$id]['#attributes']['class'][] = 'draggable';
}
$form['displays'][$id]['title'] = array(
'#markup' => $display['display_title'],
);
$form['displays'][$id]['weight'] = array(
'#type' => 'weight',
'#value' => $display['position'],
'#delta' => $count,
'#title' => $this->t('Weight for @display', array('@display' => $display['display_title'])),
'#title_display' => 'invisible',
'#attributes' => array(
'class' => array('weight'),
),
);
$form['displays'][$id]['removed'] = array(
'checkbox' => array(
'#title' => t('Remove @id', array('@id' => $id)),
'#title_display' => 'invisible',
'#type' => 'checkbox',
'#id' => 'display-removed-' . $id,
'#attributes' => array(
'class' => array('views-remove-checkbox'),
),
'#default_value' => !empty($display['deleted']),
),
'link' => array(
'#type' => 'link',
'#title' => SafeMarkup::format('<span>@text</span>', array('@text' => $this->t('Remove'))),
'#url' => Url::fromRoute('<none>'),
'#attributes' => array(
'id' => 'display-remove-link-' . $id,
'class' => array('views-button-remove', 'display-remove-link'),
'alt' => $this->t('Remove this display'),
'title' => $this->t('Remove this display'),
),
),
'#access' => ($id !== 'default'),
);
if (!empty($display['deleted'])) {
$form['displays'][$id]['deleted'] = array(
'#type' => 'value',
'#value' => TRUE,
);
$form['displays'][$id]['#attributes']['class'][] = 'hidden';
}
}
$view->getStandardButtons($form, $form_state, 'views_ui_reorder_displays_form');
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
/** @var $view \Drupal\views_ui\ViewUI */
$view = $form_state->get('view');
$order = array();
$user_input = $form_state->getUserInput();
foreach ($user_input['displays'] as $display => $info) {
// Add each value that is a field with a weight to our list, but only if
// it has had its 'removed' checkbox checked.
if (is_array($info) && isset($info['weight']) && empty($info['removed']['checkbox'])) {
$order[$display] = $info['weight'];
}
}
// Sort the order array.
asort($order);
// Remove the default display from ordering.
unset($order['default']);
// Increment up positions.
$position = 1;
foreach (array_keys($order) as $display) {
$order[$display] = $position++;
}
// Setting up position and removing deleted displays.
$displays = $view->get('display');
foreach ($displays as $display_id => &$display) {
// Don't touch the default.
if ($display_id === 'default') {
$display['position'] = 0;
continue;
}
if (isset($order[$display_id])) {
$display['position'] = $order[$display_id];
}
else {
$display['deleted'] = TRUE;
}
}
$view->set('display', $displays);
// Store in cache.
$view->cacheSet();
$url = $view->urlInfo('edit-form')
->setOption('fragment', 'views-tab-default');
$form_state->setRedirectUrl($url);
}
}

View file

@ -0,0 +1,278 @@
<?php
namespace Drupal\views_ui\Form\Ajax;
use Drupal\Component\Utility\Html;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\CloseModalDialogCommand;
use Drupal\Core\Ajax\OpenModalDialogCommand;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormState;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\Core\Render\RenderContext;
use Drupal\views\Ajax\HighlightCommand;
use Drupal\views\Ajax\ReplaceTitleCommand;
use Drupal\views\Ajax\ShowButtonsCommand;
use Drupal\views\Ajax\TriggerPreviewCommand;
use Drupal\views\ViewEntityInterface;
use Drupal\views_ui\Ajax\SetFormCommand;
use Symfony\Component\HttpFoundation\RedirectResponse;
/**
* Provides a base class for Views UI AJAX forms.
*/
abstract class ViewsFormBase extends FormBase implements ViewsFormInterface {
/**
* The ID of the item this form is manipulating.
*
* @var string
*/
protected $id;
/**
* The type of item this form is manipulating.
*
* @var string
*/
protected $type;
/**
* Sets the ID for this form.
*
* @param string $id
* The ID of the item this form is manipulating.
*/
protected function setID($id) {
if ($id) {
$this->id = $id;
}
}
/**
* Sets the type for this form.
*
* @param string $type
* The type of the item this form is manipulating.
*/
protected function setType($type) {
if ($type) {
$this->type = $type;
}
}
/**
* {@inheritdoc}
*/
public function getFormState(ViewEntityInterface $view, $display_id, $js) {
// $js may already have been converted to a Boolean.
$ajax = is_string($js) ? $js === 'ajax' : $js;
return (new FormState())
->set('form_id', $this->getFormId())
->set('form_key', $this->getFormKey())
->set('ajax', $ajax)
->set('display_id', $display_id)
->set('view', $view)
->set('type', $this->type)
->set('id', $this->id)
->disableRedirect()
->addBuildInfo('callback_object', $this);
}
/**
* {@inheritdoc}
*/
public function getForm(ViewEntityInterface $view, $display_id, $js) {
$form_state = $this->getFormState($view, $display_id, $js);
$view = $form_state->get('view');
$key = $form_state->get('form_key');
// @todo Remove the need for this.
\Drupal::moduleHandler()->loadInclude('views_ui', 'inc', 'admin');
// Reset the cache of IDs. Drupal rather aggressively prevents ID
// duplication but this causes it to remember IDs that are no longer even
// being used.
Html::resetSeenIds();
// check to see if this is the top form of the stack. If it is, pop
// it off; if it isn't, the user clicked somewhere else and the stack is
// now irrelevant.
if (!empty($view->stack)) {
$identifier = implode('-', array_filter([$key, $view->id(), $display_id, $form_state->get('type'), $form_state->get('id')]));
// Retrieve the first form from the stack without changing the integer keys,
// as they're being used for the "2 of 3" progress indicator.
reset($view->stack);
list($key, $top) = each($view->stack);
unset($view->stack[$key]);
if (array_shift($top) != $identifier) {
$view->stack = array();
}
}
// Automatically remove the form cache if it is set and the key does
// not match. This way navigating away from the form without hitting
// update will work.
if (isset($view->form_cache) && $view->form_cache['key'] != $key) {
unset($view->form_cache);
}
$form_class = get_class($form_state->getFormObject());
$response = $this->ajaxFormWrapper($form_class, $form_state);
// If the form has not been submitted, or was not set for rerendering, stop.
if (!$form_state->isSubmitted() || $form_state->get('rerender')) {
return $response;
}
// Sometimes we need to re-generate the form for multi-step type operations.
if (!empty($view->stack)) {
$stack = $view->stack;
$top = array_shift($stack);
// Build the new form state for the next form in the stack.
$reflection = new \ReflectionClass($view::$forms[$top[1]]);
/** @var $form_state \Drupal\Core\Form\FormStateInterface */
$form_state = $reflection->newInstanceArgs(array_slice($top, 3, 2))->getFormState($view, $top[2], $form_state->get('ajax'));
$form_class = get_class($form_state->getFormObject());
$form_state->setUserInput(array());
$form_url = views_ui_build_form_url($form_state);
if (!$form_state->get('ajax')) {
return new RedirectResponse($form_url->setAbsolute()->toString());
}
$form_state->set('url', $form_url);
$response = $this->ajaxFormWrapper($form_class, $form_state);
}
elseif (!$form_state->get('ajax')) {
// if nothing on the stack, non-js forms just go back to the main view editor.
$display_id = $form_state->get('display_id');
return new RedirectResponse($this->url('entity.view.edit_display_form', ['view' => $view->id(), 'display_id' => $display_id], ['absolute' => TRUE]));
}
else {
$response = new AjaxResponse();
$response->addCommand(new CloseModalDialogCommand());
$response->addCommand(new ShowButtonsCommand(!empty($view->changed)));
$response->addCommand(new TriggerPreviewCommand());
if ($page_title = $form_state->get('page_title')) {
$response->addCommand(new ReplaceTitleCommand($page_title));
}
}
// If this form was for view-wide changes, there's no need to regenerate
// the display section of the form.
if ($display_id !== '') {
\Drupal::entityManager()->getFormObject('view', 'edit')->rebuildCurrentTab($view, $response, $display_id);
}
return $response;
}
/**
* Wrapper for handling AJAX forms.
*
* Wrapper around \Drupal\Core\Form\FormBuilderInterface::buildForm() to
* handle some AJAX stuff automatically.
* This makes some assumptions about the client.
*
* @param \Drupal\Core\Form\FormInterface|string $form_class
* The value must be one of the following:
* - The name of a class that implements \Drupal\Core\Form\FormInterface.
* - An instance of a class that implements \Drupal\Core\Form\FormInterface.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*
* @return \Drupal\Core\Ajax\AjaxResponse|string|array
* Returns one of three possible values:
* - A \Drupal\Core\Ajax\AjaxResponse object.
* - The rendered form, as a string.
* - A render array with the title in #title and the rendered form in the
* #markup array.
*/
protected function ajaxFormWrapper($form_class, FormStateInterface &$form_state) {
/** @var \Drupal\Core\Render\RendererInterface $renderer */
$renderer = \Drupal::service('renderer');
// This won't override settings already in.
if (!$form_state->has('rerender')) {
$form_state->set('rerender', FALSE);
}
$ajax = $form_state->get('ajax');
// Do not overwrite if the redirect has been disabled.
if (!$form_state->isRedirectDisabled()) {
$form_state->disableRedirect($ajax);
}
$form_state->disableCache();
// Builds the form in a render context in order to ensure that cacheable
// metadata is bubbled up.
$render_context = new RenderContext();
$callable = function () use ($form_class, &$form_state) {
return \Drupal::formBuilder()->buildForm($form_class, $form_state);
};
$form = $renderer->executeInRenderContext($render_context, $callable);
if (!$render_context->isEmpty()) {
BubbleableMetadata::createFromRenderArray($form)
->merge($render_context->pop())
->applyTo($form);
}
$output = $renderer->renderRoot($form);
// These forms have the title built in, so set the title here:
$title = $form_state->get('title') ?: '';
if ($ajax && (!$form_state->isExecuted() || $form_state->get('rerender'))) {
// If the form didn't execute and we're using ajax, build up an
// Ajax command list to execute.
$response = new AjaxResponse();
// Attach the library necessary for using the OpenModalDialogCommand and
// set the attachments for this Ajax response.
$form['#attached']['library'][] = 'core/drupal.dialog.ajax';
$response->setAttachments($form['#attached']);
$display = '';
$status_messages = array('#type' => 'status_messages');
if ($messages = $renderer->renderRoot($status_messages)) {
$display = '<div class="views-messages">' . $messages . '</div>';
}
$display .= $output;
$options = array(
'dialogClass' => 'views-ui-dialog js-views-ui-dialog',
'width' => '75%',
);
$response->addCommand(new OpenModalDialogCommand($title, $display, $options));
// Views provides its own custom handling of AJAX form submissions.
// Usually this happens at the same path, but custom paths may be
// specified in $form_state.
$form_url = $form_state->has('url') ? $form_state->get('url')->toString() : $this->url('<current>');
$response->addCommand(new SetFormCommand($form_url));
if ($section = $form_state->get('#section')) {
$response->addCommand(new HighlightCommand('.' . Html::cleanCssIdentifier($section)));
}
return $response;
}
return $title ? ['#title' => $title, '#markup' => $output] : $output;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
}
}

View file

@ -0,0 +1,55 @@
<?php
namespace Drupal\views_ui\Form\Ajax;
use Drupal\Core\Form\FormInterface;
use Drupal\views\ViewEntityInterface;
interface ViewsFormInterface extends FormInterface {
/**
* Returns the key that represents this form.
*
* @return string
* The form key used in the URL, e.g., the string 'add-handler' in
* 'admin/structure/views/%/add-handler/%/%/%'.
*/
public function getFormKey();
/**
* Gets the form state for this form.
*
* @param \Drupal\views\ViewEntityInterface $view
* The view being edited.
* @param string|null $display_id
* The display ID being edited, or NULL to load the first available display.
* @param string $js
* If this is an AJAX form, it will be the string 'ajax'. Otherwise, it will
* be 'nojs'. This determines the response.
*
* @return \Drupal\Core\Form\FormStateInterface
* The current state of the form.
*/
public function getFormState(ViewEntityInterface $view, $display_id, $js);
/**
* Creates a new instance of this form.
*
* @param \Drupal\views\ViewEntityInterface $view
* The view being edited.
* @param string|null $display_id
* The display ID being edited, or NULL to load the first available display.
* @param string $js
* If this is an AJAX form, it will be the string 'ajax'. Otherwise, it will
* be 'nojs'. This determines the response.
*
* @return array
* An form for a specific operation in the Views UI, or an array of AJAX
* commands to render a form.
*
* @todo When https://www.drupal.org/node/1843224 is in, this will return
* \Drupal\Core\Ajax\AjaxResponse instead of the array of AJAX commands.
*/
public function getForm(ViewEntityInterface $view, $display_id, $js);
}

View file

@ -0,0 +1,189 @@
<?php
namespace Drupal\views_ui\Form;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Form builder for the admin display defaults page.
*/
class BasicSettingsForm extends ConfigFormBase {
/**
* The theme handler.
*
* @var \Drupal\Core\Extension\ThemeHandlerInterface
*/
protected $themeHandler;
/**
* Constructs a \Drupal\views_ui\Form\BasicSettingsForm object.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The factory for configuration objects.
* @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
* The theme handler.
*/
public function __construct(ConfigFactoryInterface $config_factory, ThemeHandlerInterface $theme_handler) {
parent::__construct($config_factory);
$this->themeHandler = $theme_handler;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('config.factory'),
$container->get('theme_handler')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'views_ui_admin_settings_basic';
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return ['views.settings'];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form = parent::buildForm($form, $form_state);
$config = $this->config('views.settings');
$options = array();
foreach ($this->themeHandler->listInfo() as $name => $theme) {
if ($theme->status) {
$options[$name] = $theme->info['name'];
}
}
// This is not currently a fieldset but we may want it to be later,
// so this will make it easier to change if we do.
$form['basic'] = array();
$form['basic']['ui_show_master_display'] = array(
'#type' => 'checkbox',
'#title' => $this->t('Always show the master (default) display'),
'#default_value' => $config->get('ui.show.master_display'),
);
$form['basic']['ui_show_advanced_column'] = array(
'#type' => 'checkbox',
'#title' => $this->t('Always show advanced display settings'),
'#default_value' => $config->get('ui.show.advanced_column'),
);
$form['basic']['ui_show_display_embed'] = array(
'#type' => 'checkbox',
'#title' => t('Allow embedded displays'),
'#description' => t('Embedded displays can be used in code via views_embed_view().'),
'#default_value' => $config->get('ui.show.display_embed'),
);
$form['basic']['ui_exposed_filter_any_label'] = array(
'#type' => 'select',
'#title' => $this->t('Label for "Any" value on non-required single-select exposed filters'),
'#options' => array('old_any' => '<Any>', 'new_any' => $this->t('- Any -')),
'#default_value' => $config->get('ui.exposed_filter_any_label'),
);
$form['live_preview'] = array(
'#type' => 'details',
'#title' => $this->t('Live preview settings'),
'#open' => TRUE,
);
$form['live_preview']['ui_always_live_preview'] = array(
'#type' => 'checkbox',
'#title' => $this->t('Automatically update preview on changes'),
'#default_value' => $config->get('ui.always_live_preview'),
);
$form['live_preview']['ui_show_preview_information'] = array(
'#type' => 'checkbox',
'#title' => $this->t('Show information and statistics about the view during live preview'),
'#default_value' => $config->get('ui.show.preview_information'),
);
$form['live_preview']['options'] = array(
'#type' => 'container',
'#states' => array(
'visible' => array(
':input[name="ui_show_preview_information"]' => array('checked' => TRUE),
),
),
);
$form['live_preview']['options']['ui_show_sql_query_enabled'] = array(
'#type' => 'checkbox',
'#title' => $this->t('Show the SQL query'),
'#default_value' => $config->get('ui.show.sql_query.enabled'),
);
$form['live_preview']['options']['ui_show_sql_query_where'] = array(
'#type' => 'radios',
'#states' => array(
'visible' => array(
':input[name="ui_show_sql_query_enabled"]' => array('checked' => TRUE),
),
),
'#title' => t('Show SQL query'),
'#options' => array(
'above' => $this->t('Above the preview'),
'below' => $this->t('Below the preview'),
),
'#default_value' => $config->get('ui.show.sql_query.where'),
);
$form['live_preview']['options']['ui_show_performance_statistics'] = array(
'#type' => 'checkbox',
'#title' => $this->t('Show performance statistics'),
'#default_value' => $config->get('ui.show.performance_statistics'),
);
$form['live_preview']['options']['ui_show_additional_queries'] = array(
'#type' => 'checkbox',
'#title' => $this->t('Show other queries run during render during live preview'),
'#description' => $this->t("Drupal has the potential to run many queries while a view is being rendered. Checking this box will display every query run during view render as part of the live preview."),
'#default_value' => $config->get('ui.show.additional_queries'),
);
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->config('views.settings')
->set('ui.show.master_display', $form_state->getValue('ui_show_master_display'))
->set('ui.show.advanced_column', $form_state->getValue('ui_show_advanced_column'))
->set('ui.show.display_embed', $form_state->getValue('ui_show_display_embed'))
->set('ui.exposed_filter_any_label', $form_state->getValue('ui_exposed_filter_any_label'))
->set('ui.always_live_preview', $form_state->getValue('ui_always_live_preview'))
->set('ui.show.preview_information', $form_state->getValue('ui_show_preview_information'))
->set('ui.show.sql_query.where', $form_state->getValue('ui_show_sql_query_where'))
->set('ui.show.sql_query.enabled', $form_state->getValue('ui_show_sql_query_enabled'))
->set('ui.show.performance_statistics', $form_state->getValue('ui_show_performance_statistics'))
->set('ui.show.additional_queries', $form_state->getValue('ui_show_additional_queries'))
->save();
parent::submitForm($form, $form_state);
}
}

View file

@ -0,0 +1,114 @@
<?php
namespace Drupal\views_ui\Form;
use Drupal\Core\Entity\EntityConfirmFormBase;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\user\SharedTempStoreFactory;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Builds the form to break the lock of an edited view.
*/
class BreakLockForm extends EntityConfirmFormBase {
/**
* Stores the Entity manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* Stores the user tempstore.
*
* @var \Drupal\user\SharedTempStore
*/
protected $tempStore;
/**
* Constructs a \Drupal\views_ui\Form\BreakLockForm object.
*
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The Entity manager.
* @param \Drupal\user\SharedTempStoreFactory $temp_store_factory
* The factory for the temp store object.
*/
public function __construct(EntityManagerInterface $entity_manager, SharedTempStoreFactory $temp_store_factory) {
$this->entityManager = $entity_manager;
$this->tempStore = $temp_store_factory->get('views');
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity.manager'),
$container->get('user.shared_tempstore')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'views_ui_break_lock_confirm';
}
/**
* {@inheritdoc}
*/
public function getQuestion() {
return $this->t('Do you want to break the lock on view %name?', array('%name' => $this->entity->id()));
}
/**
* {@inheritdoc}
*/
public function getDescription() {
$locked = $this->tempStore->getMetadata($this->entity->id());
$account = $this->entityManager->getStorage('user')->load($locked->owner);
$username = array(
'#theme' => 'username',
'#account' => $account,
);
return $this->t('By breaking this lock, any unsaved changes made by @user will be lost.', array('@user' => drupal_render($username)));
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return $this->entity->urlInfo('edit-form');
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return $this->t('Break lock');
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
if (!$this->tempStore->getMetadata($this->entity->id())) {
$form['message']['#markup'] = $this->t('There is no lock on view %name to break.', array('%name' => $this->entity->id()));
return $form;
}
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->tempStore->delete($this->entity->id());
$form_state->setRedirectUrl($this->entity->urlInfo('edit-form'));
drupal_set_message($this->t('The lock has been broken and you may now edit this view.'));
}
}

View file

@ -0,0 +1,102 @@
<?php
namespace Drupal\views_ui\ParamConverter;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\ParamConverter\AdminPathConfigEntityConverter;
use Drupal\Core\Routing\AdminContext;
use Symfony\Component\Routing\Route;
use Drupal\Core\ParamConverter\ParamConverterInterface;
use Drupal\user\SharedTempStoreFactory;
use Drupal\views_ui\ViewUI;
/**
* Provides upcasting for a view entity to be used in the Views UI.
*
* Example:
*
* pattern: '/some/{view}/and/{bar}'
* options:
* parameters:
* view:
* type: 'entity:view'
* tempstore: TRUE
*
* The value for {view} will be converted to a view entity prepared for the
* Views UI and loaded from the views temp store, but it will not touch the
* value for {bar}.
*/
class ViewUIConverter extends AdminPathConfigEntityConverter implements ParamConverterInterface {
/**
* Stores the tempstore factory.
*
* @var \Drupal\user\SharedTempStoreFactory
*/
protected $tempStoreFactory;
/**
* Constructs a new ViewUIConverter.
*
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
* @param \Drupal\user\SharedTempStoreFactory $temp_store_factory
* The factory for the temp store object.
*/
public function __construct(EntityManagerInterface $entity_manager, SharedTempStoreFactory $temp_store_factory, ConfigFactoryInterface $config_factory = NULL, AdminContext $admin_context = NULL) {
// The config factory and admin context are new arguments due to changing
// the parent. Avoid an error on updated sites by falling back to getting
// them from the container.
// @todo Remove in 8.2.x in https://www.drupal.org/node/2674328.
if (!$config_factory) {
$config_factory = \Drupal::configFactory();
}
if (!$admin_context) {
$admin_context = \Drupal::service('router.admin_context');
}
parent::__construct($entity_manager, $config_factory, $admin_context);
$this->tempStoreFactory = $temp_store_factory;
}
/**
* {@inheritdoc}
*/
public function convert($value, $definition, $name, array $defaults) {
if (!$entity = parent::convert($value, $definition, $name, $defaults)) {
return;
}
// Get the temp store for this variable if it needs one. Attempt to load the
// view from the temp store, synchronize its status with the existing view,
// and store the lock metadata.
$store = $this->tempStoreFactory->get('views');
if ($view = $store->get($value)) {
if ($entity->status()) {
$view->enable();
}
else {
$view->disable();
}
$view->lock = $store->getMetadata($value);
}
// Otherwise, decorate the existing view for use in the UI.
else {
$view = new ViewUI($entity);
}
return $view;
}
/**
* {@inheritdoc}
*/
public function applies($definition, $name, Route $route) {
if (parent::applies($definition, $name, $route)) {
return !empty($definition['tempstore']) && $definition['type'] === 'entity:view';
}
return FALSE;
}
}

View file

@ -0,0 +1,88 @@
<?php
// @codingStandardsIgnoreFile
/**
* This file was generated via php core/scripts/generate-proxy-class.php 'Drupal\views_ui\ParamConverter\ViewUIConverter' "core/modules/views_ui/src".
*/
namespace Drupal\views_ui\ProxyClass\ParamConverter {
/**
* Provides a proxy class for \Drupal\views_ui\ParamConverter\ViewUIConverter.
*
* @see \Drupal\Component\ProxyBuilder
*/
class ViewUIConverter implements \Drupal\Core\ParamConverter\ParamConverterInterface
{
use \Drupal\Core\DependencyInjection\DependencySerializationTrait;
/**
* The id of the original proxied service.
*
* @var string
*/
protected $drupalProxyOriginalServiceId;
/**
* The real proxied service, after it was lazy loaded.
*
* @var \Drupal\views_ui\ParamConverter\ViewUIConverter
*/
protected $service;
/**
* The service container.
*
* @var \Symfony\Component\DependencyInjection\ContainerInterface
*/
protected $container;
/**
* Constructs a ProxyClass Drupal proxy object.
*
* @param \Symfony\Component\DependencyInjection\ContainerInterface $container
* The container.
* @param string $drupal_proxy_original_service_id
* The service ID of the original service.
*/
public function __construct(\Symfony\Component\DependencyInjection\ContainerInterface $container, $drupal_proxy_original_service_id)
{
$this->container = $container;
$this->drupalProxyOriginalServiceId = $drupal_proxy_original_service_id;
}
/**
* Lazy loads the real service from the container.
*
* @return object
* Returns the constructed real service.
*/
protected function lazyLoadItself()
{
if (!isset($this->service)) {
$this->service = $this->container->get($this->drupalProxyOriginalServiceId);
}
return $this->service;
}
/**
* {@inheritdoc}
*/
public function convert($value, $definition, $name, array $defaults)
{
return $this->lazyLoadItself()->convert($value, $definition, $name, $defaults);
}
/**
* {@inheritdoc}
*/
public function applies($definition, $name, \Symfony\Component\Routing\Route $route)
{
return $this->lazyLoadItself()->applies($definition, $name, $route);
}
}
}

View file

@ -0,0 +1,59 @@
<?php
namespace Drupal\views_ui\Tests;
use Drupal\views\Tests\ViewTestBase;
/**
* Tests the views analyze system.
*
* @group views_ui
*/
class AnalyzeTest extends ViewTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('views_ui');
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = array('test_view');
protected function setUp() {
parent::setUp();
$this->enableViewsTestModule();
// Add an admin user will full rights;
$this->admin = $this->drupalCreateUser(array('administer views'));
}
/**
* Tests that analyze works in general.
*/
function testAnalyzeBasic() {
$this->drupalLogin($this->admin);
$this->drupalGet('admin/structure/views/view/test_view/edit');
$this->assertLink(t('Analyze view'));
// This redirects the user to the analyze form.
$this->clickLink(t('Analyze view'));
$this->assertText(t('View analysis'));
foreach (array('ok', 'warning', 'error') as $type) {
$xpath = $this->xpath('//div[contains(@class, :class)]', array(':class' => $type));
$this->assertTrue(count($xpath), format_string('Analyse messages with @type found', array('@type' => $type)));
}
// This redirects the user back to the main views edit page.
$this->drupalPostForm(NULL, array(), t('Ok'));
}
}

View file

@ -0,0 +1,101 @@
<?php
namespace Drupal\views_ui\Tests;
use Drupal\block\Entity\Block;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\views\Entity\View;
/**
* Tests the entity area UI test.
*
* @see \Drupal\views\Plugin\views\area\Entity
* @group views_ui
*/
class AreaEntityUITest extends UITestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['entity_test'];
public function testUI() {
// Set up a block and a entity_test entity.
$block = Block::create(['id' => 'test_id', 'plugin' => 'system_main_block']);
$block->save();
$entity_test = EntityTest::create(['bundle' => 'entity_test']);
$entity_test->save();
$default = $this->randomView([]);
$id = $default['id'];
$view = View::load($id);
$this->drupalGet($view->urlInfo('edit-form'));
// Add a global NULL argument to the view for testing argument placeholders.
$this->drupalPostForm("admin/structure/views/nojs/add-handler/$id/page_1/argument", ['name[views.null]' => 1], 'Add and configure contextual filters');
$this->drupalPostForm(NULL, [], 'Apply');
// Configure both the entity_test area header and the block header to
// reference the given entities.
$this->drupalPostForm("admin/structure/views/nojs/add-handler/$id/page_1/header", ['name[views.entity_block]' => 1], 'Add and configure header');
$this->drupalPostForm(NULL, ['options[target]' => $block->id()], 'Apply');
$this->drupalPostForm("admin/structure/views/nojs/add-handler/$id/page_1/header", ['name[views.entity_entity_test]' => 1], 'Add and configure header');
$this->drupalPostForm(NULL, ['options[target]' => $entity_test->id()], 'Apply');
$this->drupalPostForm(NULL, [], 'Save');
// Confirm the correct target identifiers were saved for both entities.
$view = View::load($id);
$header = $view->getDisplay('default')['display_options']['header'];
$this->assertEqual(['entity_block', 'entity_entity_test'], array_keys($header));
$this->assertEqual($block->id(), $header['entity_block']['target']);
$this->assertEqual($entity_test->uuid(), $header['entity_entity_test']['target']);
// Confirm that the correct serial ID (for the entity_test) and config ID
// (for the block) are displayed in the form.
$this->drupalGet("admin/structure/views/nojs/handler/$id/page_1/header/entity_block");
$this->assertFieldByName('options[target]', $block->id());
$this->drupalGet("admin/structure/views/nojs/handler/$id/page_1/header/entity_entity_test");
$this->assertFieldByName('options[target]', $entity_test->id());
// Replace the header target entities with argument placeholders.
$this->drupalPostForm("admin/structure/views/nojs/handler/$id/page_1/header/entity_block", ['options[target]' => '{{ raw_arguments.null }}'], 'Apply');
$this->drupalPostForm("admin/structure/views/nojs/handler/$id/page_1/header/entity_entity_test", ['options[target]' => '{{ raw_arguments.null }}'], 'Apply');
$this->drupalPostForm(NULL, [], 'Save');
// Confirm that the argument placeholders are saved.
$view = View::load($id);
$header = $view->getDisplay('default')['display_options']['header'];
$this->assertEqual(['entity_block', 'entity_entity_test'], array_keys($header));
$this->assertEqual('{{ raw_arguments.null }}', $header['entity_block']['target']);
$this->assertEqual('{{ raw_arguments.null }}', $header['entity_entity_test']['target']);
// Confirm that the argument placeholders are still displayed in the form.
$this->drupalGet("admin/structure/views/nojs/handler/$id/page_1/header/entity_block");
$this->assertFieldByName('options[target]', '{{ raw_arguments.null }}');
$this->drupalGet("admin/structure/views/nojs/handler/$id/page_1/header/entity_entity_test");
$this->assertFieldByName('options[target]', '{{ raw_arguments.null }}');
// Change the targets for both headers back to the entities.
$this->drupalPostForm("admin/structure/views/nojs/handler/$id/page_1/header/entity_block", ['options[target]' => $block->id()], 'Apply');
$this->drupalPostForm("admin/structure/views/nojs/handler/$id/page_1/header/entity_entity_test", ['options[target]' => $entity_test->id()], 'Apply');
$this->drupalPostForm(NULL, [], 'Save');
// Confirm the targets were again saved correctly and not skipped based on
// the previous form value.
$view = View::load($id);
$header = $view->getDisplay('default')['display_options']['header'];
$this->assertEqual(['entity_block', 'entity_entity_test'], array_keys($header));
$this->assertEqual($block->id(), $header['entity_block']['target']);
$this->assertEqual($entity_test->uuid(), $header['entity_entity_test']['target']);
}
}

View file

@ -0,0 +1,57 @@
<?php
namespace Drupal\views_ui\Tests;
use Drupal\views\Views;
/**
* Tests the Argument validator through the UI.
*
* @group views_ui
*/
class ArgumentValidatorTest extends UITestBase {
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = array('test_argument');
/**
* Tests the 'Specify validation criteria' checkbox functionality.
*/
public function testSpecifyValidation() {
// Specify a validation based on Node for the 'id' argument on the default
// display and assert that this works.
$this->saveArgumentHandlerWithValidationOptions(TRUE);
$view = Views::getView('test_argument');
$handler = $view->getHandler('default', 'argument', 'id');
$this->assertTrue($handler['specify_validation'], 'Validation for this argument has been turned on.');
$this->assertEqual('entity:node', $handler['validate']['type'], 'Validation for the argument is based on the node.');
// Uncheck the 'Specify validation criteria' checkbox and expect the
// validation type to be reset back to 'none'.
$this->saveArgumentHandlerWithValidationOptions(FALSE);
$view = Views::getView('test_argument');
$handler = $view->getHandler('default', 'argument', 'id');
$this->assertFalse($handler['specify_validation'], 'Validation for this argument has been turned off.');
$this->assertEqual('none', $handler['validate']['type'], 'Validation for the argument has been reverted to Basic Validation.');
}
/**
* Saves the test_argument view with changes made to the argument handler
* both with and without specify_validation turned on.
*
* @param bool $specify_validation
*/
protected function saveArgumentHandlerWithValidationOptions($specify_validation) {
$options = array(
'options[validate][type]' => 'entity---node',
'options[specify_validation]' => $specify_validation,
);
$this->drupalPostForm('admin/structure/views/nojs/handler/test_argument/default/argument/id', $options, t('Apply'));
$this->drupalPostForm('admin/structure/views/view/test_argument', array(), t('Save'));
}
}

View file

@ -0,0 +1,74 @@
<?php
namespace Drupal\views_ui\Tests;
/**
* Tests the user tempstore cache in the UI.
*
* @group views_ui
*/
class CachedDataUITest extends UITestBase {
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = array('test_view');
/**
* Tests the user tempstore views data in the UI.
*/
public function testCacheData() {
$views_admin_user_uid = $this->fullAdminUser->id();
$temp_store = $this->container->get('user.shared_tempstore')->get('views');
// The view should not be locked.
$this->assertEqual($temp_store->getMetadata('test_view'), NULL, 'The view is not locked.');
$this->drupalGet('admin/structure/views/view/test_view/edit');
// Make sure we have 'changes' to the view.
$this->drupalPostForm('admin/structure/views/nojs/display/test_view/default/title', array(), t('Apply'));
$this->assertText('You have unsaved changes.');
$this->assertEqual($temp_store->getMetadata('test_view')->owner, $views_admin_user_uid, 'View cache has been saved.');
$view_cache = $temp_store->get('test_view');
// The view should be enabled.
$this->assertTrue($view_cache->status(), 'The view is enabled.');
// The view should now be locked.
$this->assertEqual($temp_store->getMetadata('test_view')->owner, $views_admin_user_uid, 'The view is locked.');
// Cancel the view edit and make sure the cache is deleted.
$this->drupalPostForm(NULL, array(), t('Cancel'));
$this->assertEqual($temp_store->getMetadata('test_view'), NULL, 'User tempstore data has been removed.');
// Test we are redirected to the view listing page.
$this->assertUrl('admin/structure/views', array(), 'Redirected back to the view listing page.');
// Log in with another user and make sure the view is locked and break.
$this->drupalPostForm('admin/structure/views/nojs/display/test_view/default/title', array(), t('Apply'));
$this->drupalLogin($this->adminUser);
$this->drupalGet('admin/structure/views/view/test_view/edit');
// Test that save and cancel buttons are not shown.
$this->assertNoFieldById('edit-actions-submit', t('Save'));
$this->assertNoFieldById('edit-actions-cancel', t('Cancel'));
// Test we have the break lock link.
$this->assertLinkByHref('admin/structure/views/view/test_view/break-lock');
// Break the lock.
$this->clickLink(t('break this lock'));
$this->drupalPostForm(NULL, array(), t('Break lock'));
// Test that save and cancel buttons are shown.
$this->assertFieldById('edit-actions-submit', t('Save'));
$this->assertFieldById('edit-actions-cancel', t('Cancel'));
// Test we can save the view.
$this->drupalPostForm('admin/structure/views/view/test_view/edit', array(), t('Save'));
$this->assertRaw(t('The view %view has been saved.', array('%view' => 'Test view')));
// Test that a deleted view has no tempstore data.
$this->drupalPostForm('admin/structure/views/nojs/display/test_view/default/title', array(), t('Apply'));
$this->drupalPostForm('admin/structure/views/view/test_view/delete', array(), t('Delete'));
// No view tempstore data should be returned for this view after deletion.
$this->assertEqual($temp_store->getMetadata('test_view'), NULL, 'View tempstore data has been removed after deletion.');
}
}

View file

@ -0,0 +1,181 @@
<?php
namespace Drupal\views_ui\Tests;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\views\Views;
/**
* Tests the UI and functionality for the Custom boolean field handler options.
*
* @group views_ui
* @see \Drupal\views\Plugin\views\field\Boolean
*/
class CustomBooleanTest extends UITestBase {
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = array('test_view');
/**
* \Drupal\views\Tests\ViewTestBase::viewsData().
*/
public function viewsData() {
$data = parent::viewsData();
$data['views_test_data']['age']['field']['id'] = 'boolean';
return $data;
}
/**
* {@inheritdoc}
*/
public function dataSet() {
$data = parent::dataSet();
$data[0]['age'] = 0;
$data[3]['age'] = 0;
return $data;
}
/**
* Tests the setting and output of custom labels for boolean values.
*/
public function testCustomOption() {
// Add the boolean field handler to the test view.
$view = Views::getView('test_view');
$view->setDisplay();
$view->displayHandlers->get('default')->overrideOption('fields', array(
'age' => array(
'id' => 'age',
'table' => 'views_test_data',
'field' => 'age',
'relationship' => 'none',
'plugin_id' => 'boolean',
),
));
$view->save();
$this->executeView($view);
$custom_true = 'Yay';
$custom_false = 'Nay';
// Set up some custom value mappings for different types.
$custom_values = array(
'plain' => array(
'true' => $custom_true,
'false' => $custom_false,
'test' => 'assertTrue',
),
'allowed tag' => array(
'true' => '<p>' . $custom_true . '</p>',
'false' => '<p>' . $custom_false . '</p>',
'test' => 'assertTrue',
),
'disallowed tag' => array(
'true' => '<script>' . $custom_true . '</script>',
'false' => '<script>' . $custom_false . '</script>',
'test' => 'assertFalse',
),
);
// Run the same tests on each type.
foreach ($custom_values as $type => $values) {
$options = array(
'options[type]' => 'custom',
'options[type_custom_true]' => $values['true'],
'options[type_custom_false]' => $values['false'],
);
$this->drupalPostForm('admin/structure/views/nojs/handler/test_view/default/field/age', $options, 'Apply');
// Save the view.
$this->drupalPostForm('admin/structure/views/view/test_view', array(), 'Save');
$view = Views::getView('test_view');
$output = $view->preview();
$output = \Drupal::service('renderer')->renderRoot($output);
$this->{$values['test']}(strpos($output, $values['true']), SafeMarkup::format('Expected custom boolean TRUE value %value in output for %type', ['%value' => $values['true'], '%type' => $type]));
$this->{$values['test']}(strpos($output, $values['false']), SafeMarkup::format('Expected custom boolean FALSE value %value in output for %type', ['%value' => $values['false'], '%type' => $type]));
}
}
/**
* Tests the setting and output of custom labels for boolean values.
*/
public function testCustomOptionTemplate() {
// Install theme to test with template system.
\Drupal::service('theme_handler')->install(['views_test_theme']);
// Set the default theme for Views preview.
$this->config('system.theme')
->set('default', 'views_test_theme')
->save();
$this->assertEqual($this->config('system.theme')->get('default'), 'views_test_theme');
// Add the boolean field handler to the test view.
$view = Views::getView('test_view');
$view->setDisplay();
$view->displayHandlers->get('default')->overrideOption('fields', [
'age' => [
'id' => 'age',
'table' => 'views_test_data',
'field' => 'age',
'relationship' => 'none',
'plugin_id' => 'boolean',
],
]);
$view->save();
$this->executeView($view);
$custom_true = 'Yay';
$custom_false = 'Nay';
// Set up some custom value mappings for different types.
$custom_values = array(
'plain' => array(
'true' => $custom_true,
'false' => $custom_false,
'test' => 'assertTrue',
),
'allowed tag' => array(
'true' => '<p>' . $custom_true . '</p>',
'false' => '<p>' . $custom_false . '</p>',
'test' => 'assertTrue',
),
'disallowed tag' => array(
'true' => '<script>' . $custom_true . '</script>',
'false' => '<script>' . $custom_false . '</script>',
'test' => 'assertFalse',
),
);
// Run the same tests on each type.
foreach ($custom_values as $type => $values) {
$options = array(
'options[type]' => 'custom',
'options[type_custom_true]' => $values['true'],
'options[type_custom_false]' => $values['false'],
);
$this->drupalPostForm('admin/structure/views/nojs/handler/test_view/default/field/age', $options, 'Apply');
// Save the view.
$this->drupalPostForm('admin/structure/views/view/test_view', array(), 'Save');
$view = Views::getView('test_view');
$output = $view->preview();
$output = \Drupal::service('renderer')->renderRoot($output);
$this->{$values['test']}(strpos($output, $values['true']), SafeMarkup::format('Expected custom boolean TRUE value %value in output for %type', ['%value' => $values['true'], '%type' => $type]));
$this->{$values['test']}(strpos($output, $values['false']), SafeMarkup::format('Expected custom boolean FALSE value %value in output for %type', ['%value' => $values['false'], '%type' => $type]));
// Assert that we are using the correct template.
$this->setRawContent($output);
$this->assertText('llama', 'Loaded the correct views-view-field.html.twig template');
}
}
}

View file

@ -0,0 +1,247 @@
<?php
namespace Drupal\views_ui\Tests;
use Drupal\Core\Url;
use Drupal\user\Entity\Role;
use Drupal\user\RoleInterface;
/**
* Tests enabling, disabling, and reverting default views via the listing page.
*
* @group views_ui
*/
class DefaultViewsTest extends UITestBase {
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = array('test_view_status', 'test_page_display_menu', 'test_page_display_arguments');
protected function setUp() {
parent::setUp();
$this->drupalPlaceBlock('page_title_block');
}
/**
* Tests default views.
*/
function testDefaultViews() {
// Make sure the view starts off as disabled (does not appear on the listing
// page).
$edit_href = 'admin/structure/views/view/glossary';
$this->drupalGet('admin/structure/views');
// @todo Disabled default views do now appear on the front page. Test this
// behavior with templates instead.
// $this->assertNoLinkByHref($edit_href);
// Enable the view, and make sure it is now visible on the main listing
// page.
$this->drupalGet('admin/structure/views');
$this->clickViewsOperationLink(t('Enable'), '/glossary/');
$this->assertUrl('admin/structure/views');
$this->assertLinkByHref($edit_href);
// It should not be possible to revert the view yet.
// @todo Figure out how to handle this with the new configuration system.
// $this->assertNoLink(t('Revert'));
// $revert_href = 'admin/structure/views/view/glossary/revert';
// $this->assertNoLinkByHref($revert_href);
// Edit the view and change the title. Make sure that the new title is
// displayed.
$new_title = $this->randomMachineName(16);
$edit = array('title' => $new_title);
$this->drupalPostForm('admin/structure/views/nojs/display/glossary/page_1/title', $edit, t('Apply'));
$this->drupalPostForm('admin/structure/views/view/glossary/edit/page_1', array(), t('Save'));
$this->drupalGet('glossary');
$this->assertResponse(200);
$this->assertText($new_title);
// Save another view in the UI.
$this->drupalPostForm('admin/structure/views/nojs/display/archive/page_1/title', array(), t('Apply'));
$this->drupalPostForm('admin/structure/views/view/archive/edit/page_1', array(), t('Save'));
// Check there is an enable link. i.e. The view has not been enabled after
// editing.
$this->drupalGet('admin/structure/views');
$this->assertLinkByHref('admin/structure/views/view/archive/enable');
// Enable it again so it can be tested for access permissions.
$this->clickViewsOperationLink(t('Enable'), '/archive/');
// It should now be possible to revert the view. Do that, and make sure the
// view title we added above no longer is displayed.
// $this->drupalGet('admin/structure/views');
// $this->assertLink(t('Revert'));
// $this->assertLinkByHref($revert_href);
// $this->drupalPostForm($revert_href, array(), t('Revert'));
// $this->drupalGet('glossary');
// $this->assertNoText($new_title);
// Duplicate the view and check that the normal schema of duplicated views is used.
$this->drupalGet('admin/structure/views');
$this->clickViewsOperationLink(t('Duplicate'), '/glossary');
$edit = array(
'id' => 'duplicate_of_glossary',
);
$this->assertTitle(t('Duplicate of @label | @site-name', array('@label' => 'Glossary', '@site-name' => $this->config('system.site')->get('name'))));
$this->drupalPostForm(NULL, $edit, t('Duplicate'));
$this->assertUrl('admin/structure/views/view/duplicate_of_glossary', array(), 'The normal duplicating name schema is applied.');
// Duplicate a view and set a custom name.
$this->drupalGet('admin/structure/views');
$this->clickViewsOperationLink(t('Duplicate'), '/glossary');
$random_name = strtolower($this->randomMachineName());
$this->drupalPostForm(NULL, array('id' => $random_name), t('Duplicate'));
$this->assertUrl("admin/structure/views/view/$random_name", array(), 'The custom view name got saved.');
// Now disable the view, and make sure it stops appearing on the main view
// listing page but instead goes back to displaying on the disabled views
// listing page.
// @todo Test this behavior with templates instead.
$this->drupalGet('admin/structure/views');
$this->clickViewsOperationLink(t('Disable'), '/glossary/');
// $this->assertUrl('admin/structure/views');
// $this->assertNoLinkByHref($edit_href);
// The easiest way to verify it appears on the disabled views listing page
// is to try to click the "enable" link from there again.
$this->drupalGet('admin/structure/views');
$this->clickViewsOperationLink(t('Enable'), '/glossary/');
$this->assertUrl('admin/structure/views');
$this->assertLinkByHref($edit_href);
// Clear permissions for anonymous users to check access for default views.
Role::load(RoleInterface::ANONYMOUS_ID)->revokePermission('access content')->save();
// Test the default views disclose no data by default.
$this->drupalLogout();
$this->drupalGet('glossary');
$this->assertResponse(403);
$this->drupalGet('archive');
$this->assertResponse(403);
// Test deleting a view.
$this->drupalLogin($this->fullAdminUser);
$this->drupalGet('admin/structure/views');
$this->clickViewsOperationLink(t('Delete'), '/glossary/');
// Submit the confirmation form.
$this->drupalPostForm(NULL, array(), t('Delete'));
// Ensure the view is no longer listed.
$this->assertUrl('admin/structure/views');
$this->assertNoLinkByHref($edit_href);
// Ensure the view is no longer available.
$this->drupalGet($edit_href);
$this->assertResponse(404);
$this->assertText('Page not found');
// Delete all duplicated Glossary views.
$this->drupalGet('admin/structure/views');
$this->clickViewsOperationLink(t('Delete'), 'duplicate_of_glossary');
// Submit the confirmation form.
$this->drupalPostForm(NULL, array(), t('Delete'));
$this->drupalGet('glossary');
$this->assertResponse(200);
$this->drupalGet('admin/structure/views');
$this->clickViewsOperationLink(t('Delete'), $random_name);
// Submit the confirmation form.
$this->drupalPostForm(NULL, array(), t('Delete'));
$this->drupalGet('glossary');
$this->assertResponse(404);
$this->assertText('Page not found');
}
/**
* Tests that enabling views moves them to the correct table.
*/
function testSplitListing() {
// Build a re-usable xpath query.
$xpath = '//div[@id="views-entity-list"]/div[@class = :status]/table//tr[@title = :title]';
$arguments = array(
':status' => 'views-list-section enabled',
':title' => t('Machine name: test_view_status'),
);
$this->drupalGet('admin/structure/views');
$elements = $this->xpath($xpath, $arguments);
$this->assertIdentical(count($elements), 0, 'A disabled view is not found in the enabled views table.');
$arguments[':status'] = 'views-list-section disabled';
$elements = $this->xpath($xpath, $arguments);
$this->assertIdentical(count($elements), 1, 'A disabled view is found in the disabled views table.');
// Enable the view.
$this->clickViewsOperationLink(t('Enable'), '/test_view_status/');
$elements = $this->xpath($xpath, $arguments);
$this->assertIdentical(count($elements), 0, 'After enabling a view, it is not found in the disabled views table.');
$arguments[':status'] = 'views-list-section enabled';
$elements = $this->xpath($xpath, $arguments);
$this->assertIdentical(count($elements), 1, 'After enabling a view, it is found in the enabled views table.');
// Attempt to disable the view by path directly, with no token.
$this->drupalGet('admin/structure/views/view/test_view_status/disable');
$this->assertResponse(403);
}
/**
* Tests that page displays show the correct path.
*/
public function testPathDestination() {
$this->drupalGet('admin/structure/views');
// Check that links to views on default tabs are rendered correctly.
$this->assertLinkByHref('test_page_display_menu');
$this->assertNoLinkByHref('test_page_display_menu/default');
$this->assertLinkByHref('test_page_display_menu/local');
// Check that a dynamic path is shown as text.
$this->assertRaw('test_route_with_suffix/%/suffix');
$this->assertNoLinkByHref(Url::fromUri('base:test_route_with_suffix/%/suffix')->toString());
}
/**
* Click a link to perform an operation on a view.
*
* In general, we expect lots of links titled "enable" or "disable" on the
* various views listing pages, and they might have tokens in them. So we
* need special code to find the correct one to click.
*
* @param $label
* Text between the anchor tags of the desired link.
* @param $unique_href_part
* A unique string that is expected to occur within the href of the desired
* link. For example, if the link URL is expected to look like
* "admin/structure/views/view/glossary/*", then "/glossary/" could be
* passed as the expected unique string.
*
* @return
* The page content that results from clicking on the link, or FALSE on
* failure. Failure also results in a failed assertion.
*/
function clickViewsOperationLink($label, $unique_href_part) {
$links = $this->xpath('//a[normalize-space(text())=:label]', array(':label' => $label));
foreach ($links as $link_index => $link) {
$position = strpos($link['href'], $unique_href_part);
if ($position !== FALSE) {
$index = $link_index;
break;
}
}
$this->assertTrue(isset($index), format_string('Link to "@label" containing @part found.', array('@label' => $label, '@part' => $unique_href_part)));
if (isset($index)) {
return $this->clickLink($label, $index);
}
else {
return FALSE;
}
}
}

View file

@ -0,0 +1,61 @@
<?php
namespace Drupal\views_ui\Tests;
use Drupal\views\Views;
/**
* Tests the UI for the attachment display plugin.
*
* @group views_ui
* @see \Drupal\views\Plugin\views\display\Attachment
*/
class DisplayAttachmentTest extends UITestBase {
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = array('test_attachment_ui');
/**
* Tests the attachment UI.
*/
public function testAttachmentUI() {
$this->drupalGet('admin/structure/views/view/test_attachment_ui/edit/attachment_1');
$this->assertText(t('Not defined'), 'The right text appears if there is no attachment selection yet.');
$attachment_display_url = 'admin/structure/views/nojs/display/test_attachment_ui/attachment_1/displays';
$this->drupalGet($attachment_display_url);
// Display labels should be escaped.
$this->assertEscaped('<em>Page</em>');
foreach (array('default', 'page-1') as $display_id) {
$this->assertNoFieldChecked("edit-displays-$display_id", format_string('Make sure the @display_id can be marked as attached', array('@display_id' => $display_id)));
}
// Save the attachments and test the value on the view.
$this->drupalPostForm($attachment_display_url, array('displays[page_1]' => 1), t('Apply'));
// Options summary should be escaped.
$this->assertEscaped('<em>Page</em>');
$this->assertNoRaw('<em>Page</em>');
$result = $this->xpath('//a[@id = :id]', array(':id' => 'views-attachment-1-displays'));
$this->assertEqual($result[0]->attributes()->title, t('Page'));
$this->drupalPostForm(NULL, array(), t('Save'));
$view = Views::getView('test_attachment_ui');
$view->initDisplay();
$this->assertEqual(array_keys(array_filter($view->displayHandlers->get('attachment_1')->getOption('displays'))), array('page_1'), 'The attached displays got saved as expected');
$this->drupalPostForm($attachment_display_url, array('displays[default]' => 1, 'displays[page_1]' => 1), t('Apply'));
$result = $this->xpath('//a[@id = :id]', array(':id' => 'views-attachment-1-displays'));
$this->assertEqual($result[0]->attributes()->title, t('Multiple displays'));
$this->drupalPostForm(NULL, array(), t('Save'));
$view = Views::getView('test_attachment_ui');
$view->initDisplay();
$this->assertEqual(array_keys($view->displayHandlers->get('attachment_1')->getOption('displays')), array('default', 'page_1'), 'The attached displays got saved as expected');
}
}

View file

@ -0,0 +1,148 @@
<?php
namespace Drupal\views_ui\Tests;
use Drupal\views\Views;
/**
* Tests creation, retrieval, updating, and deletion of displays in the Web UI.
*
* @group views_ui
*/
class DisplayCRUDTest extends UITestBase {
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = array('test_display');
/**
* Modules to enable
*
* @var array
*/
public static $modules = array('contextual');
/**
* Tests adding a display.
*/
public function testAddDisplay() {
// Show the master display.
$this->config('views.settings')->set('ui.show.master_display', TRUE)->save();
$settings['page[create]'] = FALSE;
$view = $this->randomView($settings);
$path_prefix = 'admin/structure/views/view/' . $view['id'] . '/edit';
$this->drupalGet($path_prefix);
// Add a new display.
$this->drupalPostForm(NULL, array(), 'Add Page');
$this->assertLinkByHref($path_prefix . '/page_1', 0, 'Make sure after adding a display the new display appears in the UI');
$this->assertNoLink('Master*', 'Make sure the master display is not marked as changed.');
$this->assertLink('Page*', 0, 'Make sure the added display is marked as changed.');
$this->drupalPostForm("admin/structure/views/nojs/display/{$view['id']}/page_1/path", array('path' => 'test/path'), t('Apply'));
$this->drupalPostForm(NULL, array(), t('Save'));
}
/**
* Tests removing a display.
*/
public function testRemoveDisplay() {
$view = $this->randomView();
$path_prefix = 'admin/structure/views/view/' . $view['id'] . '/edit';
$this->drupalGet($path_prefix . '/default');
$this->assertNoFieldById('edit-displays-settings-settings-content-tab-content-details-top-actions-delete', 'Delete Page', 'Make sure there is no delete button on the default display.');
$this->drupalGet($path_prefix . '/page_1');
$this->assertFieldById('edit-displays-settings-settings-content-tab-content-details-top-actions-delete', 'Delete Page', 'Make sure there is a delete button on the page display.');
// Delete the page, so we can test the undo process.
$this->drupalPostForm($path_prefix . '/page_1', array(), 'Delete Page');
$this->assertFieldById('edit-displays-settings-settings-content-tab-content-details-top-actions-undo-delete', 'Undo delete of Page', 'Make sure there a undo button on the page display after deleting.');
$element = $this->xpath('//a[contains(@href, :href) and contains(@class, :class)]', array(':href' => $path_prefix . '/page_1', ':class' => 'views-display-deleted-link'));
$this->assertTrue(!empty($element), 'Make sure the display link is marked as to be deleted.');
$element = $this->xpath('//a[contains(@href, :href) and contains(@class, :class)]', array(':href' => $path_prefix . '/page_1', ':class' => 'views-display-deleted-link'));
$this->assertTrue(!empty($element), 'Make sure the display link is marked as to be deleted.');
// Undo the deleting of the display.
$this->drupalPostForm($path_prefix . '/page_1', array(), 'Undo delete of Page');
$this->assertNoFieldById('edit-displays-settings-settings-content-tab-content-details-top-actions-undo-delete', 'Undo delete of Page', 'Make sure there is no undo button on the page display after reverting.');
$this->assertFieldById('edit-displays-settings-settings-content-tab-content-details-top-actions-delete', 'Delete Page', 'Make sure there is a delete button on the page display after the reverting.');
// Now delete again and save the view.
$this->drupalPostForm($path_prefix . '/page_1', array(), 'Delete Page');
$this->drupalPostForm(NULL, array(), t('Save'));
$this->assertNoLinkByHref($path_prefix . '/page_1', 'Make sure there is no display tab for the deleted display.');
// Test deleting a display that has a modified machine name.
$view = $this->randomView();
$machine_name = 'new_machine_name';
$path_prefix = 'admin/structure/views/view/' . $view['id'] . '/edit';
$this->drupalPostForm("admin/structure/views/nojs/display/{$view['id']}/page_1/display_id", array('display_id' => $machine_name), 'Apply');
$this->drupalPostForm(NULL, array(), 'Delete Page');
$this->drupalPostForm(NULL, array(), t('Save'));
$this->assertResponse(200);
$this->assertNoLinkByHref($path_prefix . '/new_machine_name', 'Make sure there is no display tab for the deleted display.');
}
/**
* Tests that the correct display is loaded by default.
*/
public function testDefaultDisplay() {
$this->drupalGet('admin/structure/views/view/test_display');
$elements = $this->xpath('//*[@id="views-page-1-display-title"]');
$this->assertEqual(count($elements), 1, 'The page display is loaded as the default display.');
}
/**
* Tests the duplicating of a display.
*/
public function testDuplicateDisplay() {
$view = $this->randomView();
$path_prefix = 'admin/structure/views/view/' . $view['id'] . '/edit';
$path = $view['page[path]'];
$this->drupalGet($path_prefix);
$this->drupalPostForm(NULL, array(), 'Duplicate Page');
$this->assertLinkByHref($path_prefix . '/page_2', 0, 'Make sure after duplicating the new display appears in the UI');
$this->assertUrl($path_prefix . '/page_2', array(), 'The user got redirected to the new display.');
// Set the title and override the css classes.
$random_title = $this->randomMachineName();
$random_css = $this->randomMachineName();
$this->drupalPostForm("admin/structure/views/nojs/display/{$view['id']}/page_2/title", array('title' => $random_title), t('Apply'));
$this->drupalPostForm("admin/structure/views/nojs/display/{$view['id']}/page_2/css_class", array('override[dropdown]' => 'page_2', 'css_class' => $random_css), t('Apply'));
// Duplicate as a different display type.
$this->drupalPostForm(NULL, array(), 'Duplicate as Block');
$this->assertLinkByHref($path_prefix . '/block_1', 0, 'Make sure after duplicating the new display appears in the UI');
$this->assertUrl($path_prefix . '/block_1', array(), 'The user got redirected to the new display.');
$this->assertText(t('Block settings'));
$this->assertNoText(t('Page settings'));
$this->drupalPostForm(NULL, array(), t('Save'));
$view = Views::getView($view['id']);
$view->initDisplay();
$page_2 = $view->displayHandlers->get('page_2');
$this->assertTrue($page_2, 'The new page display got saved.');
$this->assertEqual($page_2->display['display_title'], 'Page');
$this->assertEqual($page_2->display['display_options']['path'], $path);
$block_1 = $view->displayHandlers->get('block_1');
$this->assertTrue($block_1, 'The new block display got saved.');
$this->assertEqual($block_1->display['display_plugin'], 'block');
$this->assertEqual($block_1->display['display_title'], 'Block', 'The new display title got generated as expected.');
$this->assertFalse(isset($block_1->display['display_options']['path']));
$this->assertEqual($block_1->getOption('title'), $random_title, 'The overridden title option from the display got copied into the duplicate');
$this->assertEqual($block_1->getOption('css_class'), $random_css, 'The overridden css_class option from the display got copied into the duplicate');
}
}

View file

@ -0,0 +1,44 @@
<?php
namespace Drupal\views_ui\Tests;
use Drupal\views\Views;
/**
* Tests the display extender UI.
*
* @group views_ui
*/
class DisplayExtenderUITest extends UITestBase {
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = array('test_view');
/**
* Tests the display extender UI.
*/
public function testDisplayExtenderUI() {
$this->config('views.settings')->set('display_extenders', array('display_extender_test'))->save();
$view = Views::getView('test_view');
$view_edit_url = "admin/structure/views/view/{$view->storage->id()}/edit";
$display_option_url = 'admin/structure/views/nojs/display/test_view/default/test_extender_test_option';
$this->drupalGet($view_edit_url);
$this->assertLinkByHref($display_option_url, 0, 'Make sure the option defined by the test display extender appears in the UI.');
$random_text = $this->randomMachineName();
$this->drupalPostForm($display_option_url, array('test_extender_test_option' => $random_text), t('Apply'));
$this->assertLink($random_text);
$this->drupalPostForm(NULL, array(), t('Save'));
$view = Views::getView($view->storage->id());
$view->initDisplay();
$display_extender_options = $view->display_handler->getOption('display_extenders');
$this->assertEqual($display_extender_options['display_extender_test']['test_extender_test_option'], $random_text, 'Make sure that the display extender option got saved.');
}
}

View file

@ -0,0 +1,84 @@
<?php
namespace Drupal\views_ui\Tests;
/**
* Tests the UI for feed display plugin.
*
* @group views_ui
* @see \Drupal\views\Plugin\views\display\Feed
*/
class DisplayFeedTest extends UITestBase {
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = array('test_display_feed', 'test_style_opml');
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('views_ui', 'aggregator');
/**
* Tests feed display admin UI.
*/
public function testFeedUI() {
// Test both RSS and OPML feeds.
foreach (self::$testViews as $view_name) {
$this->checkFeedViewUi($view_name);
}
}
/**
* Checks views UI for a specific feed view.
*
* @param string $view_name
* The view name to check against.
*/
protected function checkFeedViewUi($view_name) {
$this->drupalGet('admin/structure/views');
// Verify that the page lists the $view_name view.
// Regression test: ViewListBuilder::getDisplayPaths() did not properly
// check whether a DisplayPluginCollection was returned in iterating over
// all displays.
$this->assertText($view_name);
// Check the attach TO interface.
$this->drupalGet('admin/structure/views/nojs/display/' . $view_name . '/feed_1/displays');
// Display labels should be escaped.
$this->assertEscaped('<em>Page</em>');
// Load all the options of the checkbox.
$result = $this->xpath('//div[@id="edit-displays"]/div');
$options = array();
foreach ($result as $item) {
foreach ($item->input->attributes() as $attribute => $value) {
if ($attribute == 'value') {
$options[] = (string) $value;
}
}
}
$this->assertEqual($options, array('default', 'page'), 'Make sure all displays appears as expected.');
// Post and save this and check the output.
$this->drupalPostForm('admin/structure/views/nojs/display/' . $view_name . '/feed_1/displays', array('displays[page]' => 'page'), t('Apply'));
// Options summary should be escaped.
$this->assertEscaped('<em>Page</em>');
$this->assertNoRaw('<em>Page</em>');
$this->drupalGet('admin/structure/views/view/' . $view_name . '/edit/feed_1');
$this->assertFieldByXpath('//*[@id="views-feed-1-displays"]', '<em>Page</em>');
// Add the default display, so there should now be multiple displays.
$this->drupalPostForm('admin/structure/views/nojs/display/' . $view_name . '/feed_1/displays', array('displays[default]' => 'default'), t('Apply'));
$this->drupalGet('admin/structure/views/view/' . $view_name . '/edit/feed_1');
$this->assertFieldByXpath('//*[@id="views-feed-1-displays"]', 'Multiple displays');
}
}

View file

@ -0,0 +1,248 @@
<?php
namespace Drupal\views_ui\Tests;
use Drupal\Core\Menu\MenuTreeParameters;
use Drupal\menu_link_content\Entity\MenuLinkContent;
/**
* Tests the UI of generic display path plugin.
*
* @group views_ui
* @see \Drupal\views\Plugin\views\display\PathPluginBase
*/
class DisplayPathTest extends UITestBase {
protected function setUp() {
parent::setUp();
$this->drupalPlaceBlock('page_title_block');
}
/**
* {@inheritdoc}
*/
public static $modules = array('menu_ui');
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = array('test_view', 'test_page_display_menu');
/**
* Runs the tests.
*/
public function testPathUI() {
$this->doBasicPathUITest();
$this->doAdvancedPathsValidationTest();
$this->doPathXssFilterTest();
}
/**
* Tests basic functionality in configuring a view.
*/
protected function doBasicPathUITest() {
$this->drupalGet('admin/structure/views/view/test_view');
// Add a new page display and check the appearing text.
$this->drupalPostForm(NULL, array(), 'Add Page');
$this->assertText(t('No path is set'), 'The right text appears if no path was set.');
$this->assertNoLink(t('View @display', array('@display' => 'page')), 'No view page link found on the page.');
// Save a path and make sure the summary appears as expected.
$random_path = $this->randomMachineName();
// @todo Once https://www.drupal.org/node/2351379 is resolved, Views will no
// longer use Url::fromUri(), and this path will be able to contain ':'.
$random_path = str_replace(':', '', $random_path);
$this->drupalPostForm('admin/structure/views/nojs/display/test_view/page_1/path', array('path' => $random_path), t('Apply'));
$this->assertText('/' . $random_path, 'The custom path appears in the summary.');
$display_link_text = t('View @display', ['@display' => 'Page']);
$this->assertLink($display_link_text, 0, 'view page link found on the page.');
$this->clickLink($display_link_text);
$this->assertUrl($random_path);
}
/**
* Tests that View paths are properly filtered for XSS.
*/
public function doPathXssFilterTest() {
$this->drupalGet('admin/structure/views/view/test_view');
$this->drupalPostForm(NULL, array(), 'Add Page');
$this->drupalPostForm('admin/structure/views/nojs/display/test_view/page_2/path', array('path' => '<object>malformed_path</object>'), t('Apply'));
$this->drupalPostForm(NULL, array(), 'Add Page');
$this->drupalPostForm('admin/structure/views/nojs/display/test_view/page_3/path', array('path' => '<script>alert("hello");</script>'), t('Apply'));
$this->drupalPostForm(NULL, array(), 'Add Page');
$this->drupalPostForm('admin/structure/views/nojs/display/test_view/page_4/path', array('path' => '<script>alert("hello I have placeholders %");</script>'), t('Apply'));
$this->drupalPostForm('admin/structure/views/view/test_view', array(), t('Save'));
$this->drupalGet('admin/structure/views');
// The anchor text should be escaped.
$this->assertEscaped('/<object>malformed_path</object>');
$this->assertEscaped('/<script>alert("hello");</script>');
$this->assertEscaped('/<script>alert("hello I have placeholders %");</script>');
// Links should be url-encoded.
$this->assertRaw('/%3Cobject%3Emalformed_path%3C/object%3E');
$this->assertRaw('/%3Cscript%3Ealert%28%22hello%22%29%3B%3C/script%3E');
}
/**
* Tests a couple of invalid path patterns.
*/
protected function doAdvancedPathsValidationTest() {
$url = 'admin/structure/views/nojs/display/test_view/page_1/path';
$this->drupalPostForm($url, array('path' => '%/magrathea'), t('Apply'));
$this->assertUrl($url);
$this->assertText('"%" may not be used for the first segment of a path.');
$this->drupalPostForm($url, array('path' => 'user/%1/example'), t('Apply'));
$this->assertUrl($url);
$this->assertText("Numeric placeholders may not be used. Please use plain placeholders (%).");
}
/**
* Tests deleting a page display that has no path.
*/
public function testDeleteWithNoPath() {
$this->drupalGet('admin/structure/views/view/test_view');
$this->drupalPostForm(NULL, array(), t('Add Page'));
$this->drupalPostForm(NULL, array(), t('Delete Page'));
$this->drupalPostForm(NULL, array(), t('Save'));
$this->assertRaw(t('The view %view has been saved.', array('%view' => 'Test view')));
}
/**
* Tests the menu and tab option form.
*/
public function testMenuOptions() {
$this->container->get('module_installer')->install(array('menu_ui'));
$this->drupalGet('admin/structure/views/view/test_view');
// Add a new page display.
$this->drupalPostForm(NULL, array(), 'Add Page');
// Add an invalid path (only fragment).
$this->drupalPostForm('admin/structure/views/nojs/display/test_view/page_1/path', array('path' => '#foo'), t('Apply'));
$this->assertText('Path is empty');
// Add an invalid path with a query.
$this->drupalPostForm('admin/structure/views/nojs/display/test_view/page_1/path', array('path' => 'foo?bar'), t('Apply'));
$this->assertText('No query allowed.');
// Add an invalid path with just a query.
$this->drupalPostForm('admin/structure/views/nojs/display/test_view/page_1/path', array('path' => '?bar'), t('Apply'));
$this->assertText('Path is empty');
// Provide a random, valid path string.
$random_string = $this->randomMachineName();
// Save a path.
$this->drupalPostForm('admin/structure/views/nojs/display/test_view/page_1/path', array('path' => $random_string), t('Apply'));
$this->drupalGet('admin/structure/views/view/test_view');
$this->drupalPostForm('admin/structure/views/nojs/display/test_view/page_1/menu', array('menu[type]' => 'default tab', 'menu[title]' => 'Test tab title'), t('Apply'));
$this->assertResponse(200);
$this->assertUrl('admin/structure/views/nojs/display/test_view/page_1/tab_options');
$this->drupalPostForm(NULL, array('tab_options[type]' => 'tab', 'tab_options[title]' => $this->randomString()), t('Apply'));
$this->assertResponse(200);
$this->assertUrl('admin/structure/views/view/test_view/edit/page_1');
$this->drupalGet('admin/structure/views/view/test_view');
$this->assertLink(t('Tab: @title', array('@title' => 'Test tab title')));
// If it's a default tab, it should also have an additional settings link.
$this->assertLinkByHref('admin/structure/views/nojs/display/test_view/page_1/tab_options');
// Ensure that you can select a parent in case the parent does not exist.
$this->drupalGet('admin/structure/views/nojs/display/test_page_display_menu/page_5/menu');
$this->assertResponse(200);
$menu_parent = $this->xpath('//select[@id="edit-menu-parent"]');
$menu_options = (array) $menu_parent[0]->option;
unset($menu_options['@attributes']);
$this->assertEqual([
'<User account menu>',
'-- My account',
'-- Log out',
'<Administration>',
'<Footer>',
'<Main navigation>',
'<Tools>',
'-- Compose tips (disabled)',
'-- Test menu link',
], $menu_options);
// The cache contexts associated with the (in)accessible menu links are
// bubbled.
$this->assertCacheContext('user.permissions');
}
/**
* Tests the regression in https://www.drupal.org/node/2532490.
*/
public function testDefaultMenuTabRegression() {
$this->container->get('module_installer')->install(['menu_ui', 'menu_link_content', 'toolbar', 'system']);
$admin_user = $this->drupalCreateUser([
'administer views',
'administer blocks',
'bypass node access',
'access user profiles',
'view all revisions',
'administer permissions',
'administer menu',
'link to any page',
'access toolbar',
]);
$this->drupalLogin($admin_user);
$edit = [
'title[0][value]' => 'Menu title',
'link[0][uri]' => '/admin/foo',
'menu_parent' => 'admin:system.admin'
];
$this->drupalPostForm('admin/structure/menu/manage/admin/add', $edit, t('Save'));
$menu_items = \Drupal::entityManager()->getStorage('menu_link_content')->getQuery()
->sort('id', 'DESC')
->pager(1)
->execute();
$menu_item = end($menu_items);
/** @var \Drupal\menu_link_content\MenuLinkContentInterface $menu_link_content */
$menu_link_content = MenuLinkContent::load($menu_item);
$edit = [];
$edit['label'] = $this->randomMachineName(16);
$view_id = $edit['id'] = strtolower($this->randomMachineName(16));
$edit['description'] = $this->randomMachineName(16);
$edit['page[create]'] = TRUE;
$edit['page[path]'] = 'admin/foo';
$this->drupalPostForm('admin/structure/views/add', $edit, t('Save and edit'));
$parameters = new MenuTreeParameters();
$parameters->addCondition('id', $menu_link_content->getPluginId());
$result = \Drupal::menuTree()->load('admin', $parameters);
$plugin_definition = end($result)->link->getPluginDefinition();
$this->assertEqual('view.' . $view_id . '.page_1', $plugin_definition['route_name']);
$this->clickLink(t('No menu'));
$this->drupalPostForm(NULL, [
'menu[type]' => 'default tab',
'menu[title]' => 'Menu title',
], t('Apply'));
$this->assertText('Default tab options');
$this->drupalPostForm(NULL, [
'tab_options[type]' => 'normal',
'tab_options[title]' => 'Parent title',
], t('Apply'));
$this->drupalPostForm(NULL, [], t('Save'));
// Assert that saving the view will not cause an exception.
$this->assertResponse(200);
}
}

View file

@ -0,0 +1,331 @@
<?php
namespace Drupal\views_ui\Tests;
use Drupal\Component\Serialization\Json;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Template\Attribute;
use Drupal\views\Entity\View;
use Drupal\views\Views;
/**
* Tests the display UI.
*
* @group views_ui
*/
class DisplayTest extends UITestBase {
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = array('test_display');
/**
* Modules to enable
*
* @var array
*/
public static $modules = array('contextual');
/**
* Tests adding a display.
*/
public function testAddDisplay() {
$view = $this->randomView();
$this->assertNoText('Block');
$this->assertNoText('Block 2');
$this->drupalPostForm(NULL, [], t('Add @display', ['@display' => 'Block']));
$this->assertText('Block');
$this->assertNoText('Block 2');
// Views has special form handling in views_ui_form_button_was_clicked()
// to be able to change the submit button text via JS, this simulates what
// the JS is doing.
$this->drupalPostForm(NULL, [], NULL, [], [], NULL, '&op=Block');
$this->assertText('Block');
$this->assertText('Block 2');
}
/**
* Tests reordering of displays.
*/
public function testReorderDisplay() {
$view = array(
'block[create]' => TRUE
);
$view = $this->randomView($view);
$this->clickLink(t('Reorder displays'));
$this->assertTrue($this->xpath('//tr[@id="display-row-default"]'), 'Make sure the default display appears on the reorder listing');
$this->assertTrue($this->xpath('//tr[@id="display-row-page_1"]'), 'Make sure the page display appears on the reorder listing');
$this->assertTrue($this->xpath('//tr[@id="display-row-block_1"]'), 'Make sure the block display appears on the reorder listing');
// Ensure the view displays are in the expected order in configuration.
$expected_display_order = array('default', 'block_1', 'page_1');
$this->assertEqual(array_keys(Views::getView($view['id'])->storage->get('display')), $expected_display_order, 'The correct display names are present.');
// Put the block display in front of the page display.
$edit = array(
'displays[page_1][weight]' => 2,
'displays[block_1][weight]' => 1
);
$this->drupalPostForm(NULL, $edit, t('Apply'));
$this->drupalPostForm(NULL, array(), t('Save'));
$view = Views::getView($view['id']);
$displays = $view->storage->get('display');
$this->assertEqual($displays['default']['position'], 0, 'Make sure the master display comes first.');
$this->assertEqual($displays['block_1']['position'], 1, 'Make sure the block display comes before the page display.');
$this->assertEqual($displays['page_1']['position'], 2, 'Make sure the page display comes after the block display.');
// Ensure the view displays are in the expected order in configuration.
$this->assertEqual(array_keys($view->storage->get('display')), $expected_display_order, 'The correct display names are present.');
}
/**
* Tests disabling of a display.
*/
public function testDisableDisplay() {
$view = $this->randomView();
$path_prefix = 'admin/structure/views/view/' . $view['id'] . '/edit';
$this->drupalGet($path_prefix);
$this->assertFalse($this->xpath('//div[contains(@class, :class)]', array(':class' => 'views-display-disabled')), 'Make sure the disabled display css class does not appear after initial adding of a view.');
$this->assertFieldById('edit-displays-settings-settings-content-tab-content-details-top-actions-disable', '', 'Make sure the disable button is visible.');
$this->assertNoFieldById('edit-displays-settings-settings-content-tab-content-details-top-actions-enable', '', 'Make sure the enable button is not visible.');
$this->drupalPostForm(NULL, array(), 'Disable Page');
$this->assertTrue($this->xpath('//div[contains(@class, :class)]', array(':class' => 'views-display-disabled')), 'Make sure the disabled display css class appears once the display is marked as such.');
$this->assertNoFieldById('edit-displays-settings-settings-content-tab-content-details-top-actions-disable', '', 'Make sure the disable button is not visible.');
$this->assertFieldById('edit-displays-settings-settings-content-tab-content-details-top-actions-enable', '', 'Make sure the enable button is visible.');
$this->drupalPostForm(NULL, array(), 'Enable Page');
$this->assertFalse($this->xpath('//div[contains(@class, :class)]', array(':class' => 'views-display-disabled')), 'Make sure the disabled display css class does not appears once the display is enabled again.');
}
/**
* Tests views_ui_views_plugins_display_alter is altering plugin definitions.
*/
public function testDisplayPluginsAlter() {
$definitions = Views::pluginManager('display')->getDefinitions();
$expected = array(
'route_name' => 'entity.view.edit_form',
'route_parameters_names' => array('view' => 'id'),
);
// Test the expected views_ui array exists on each definition.
foreach ($definitions as $definition) {
$this->assertIdentical($definition['contextual links']['entity.view.edit_form'], $expected, 'Expected views_ui array found in plugin definition.');
}
}
/**
* Tests display areas.
*/
public function testDisplayAreas() {
// Show the advanced column.
$this->config('views.settings')->set('ui.show.advanced_column', TRUE)->save();
// Add a new data display to the view.
$view = Views::getView('test_display');
$view->storage->addDisplay('display_no_area_test');
$view->save();
$this->drupalGet('admin/structure/views/view/test_display/edit/display_no_area_test_1');
$areas = array(
'header',
'footer',
'empty',
);
// Assert that the expected text is found in each area category.
foreach ($areas as $type) {
$element = $this->xpath('//div[contains(@class, :class)]/div', array(':class' => $type));
$this->assertEqual((string) $element[0], SafeMarkup::format('The selected display type does not use @type plugins', array('@type' => $type)));
}
}
/**
* Tests the link-display setting.
*/
public function testLinkDisplay() {
// Test setting the link display in the UI form.
$path = 'admin/structure/views/view/test_display/edit/block_1';
$link_display_path = 'admin/structure/views/nojs/display/test_display/block_1/link_display';
// Test the link text displays 'None' and not 'Block 1'
$this->drupalGet($path);
$result = $this->xpath("//a[contains(@href, :path)]", array(':path' => $link_display_path));
$this->assertEqual($result[0], t('None'), 'Make sure that the link option summary shows "None" by default.');
$this->drupalGet($link_display_path);
$this->assertFieldChecked('edit-link-display-0');
// Test the default radio option on the link display form.
$this->drupalPostForm($link_display_path, array('link_display' => 'page_1'), t('Apply'));
// The form redirects to the master display.
$this->drupalGet($path);
$result = $this->xpath("//a[contains(@href, :path)]", array(':path' => $link_display_path));
$this->assertEqual($result[0], 'Page', 'Make sure that the link option summary shows the right linked display.');
$this->drupalPostForm($link_display_path, array('link_display' => 'custom_url', 'link_url' => 'a-custom-url'), t('Apply'));
// The form redirects to the master display.
$this->drupalGet($path);
$this->assertLink(t('Custom URL'), 0, 'The link option has custom URL as summary.');
// Test the default link_url value for new display
$this->drupalPostForm(NULL, array(), t('Add Block'));
$this->assertUrl('admin/structure/views/view/test_display/edit/block_2');
$this->clickLink(t('Custom URL'));
$this->assertFieldByName('link_url', 'a-custom-url');
}
/**
* Tests contextual links on Views page displays.
*/
public function testPageContextualLinks() {
$this->drupalLogin($this->drupalCreateUser(array('administer views', 'access contextual links')));
$view = View::load('test_display');
$view->enable()->save();
$this->container->get('router.builder')->rebuildIfNeeded();
// When no "main content" block is placed, we find a contextual link
// placeholder for editing just the view.
$this->drupalGet('test-display');
$id = 'entity.view.edit_form:view=test_display:location=page&name=test_display&display_id=page_1&langcode=en';
// @see \Drupal\contextual\Tests\ContextualDynamicContextTest:assertContextualLinkPlaceHolder()
$this->assertRaw('<div' . new Attribute(array('data-contextual-id' => $id)) . '></div>', format_string('Contextual link placeholder with id @id exists.', array('@id' => $id)));
// Get server-rendered contextual links.
// @see \Drupal\contextual\Tests\ContextualDynamicContextTest:renderContextualLinks()
$post = array('ids[0]' => $id);
$response = $this->drupalPostWithFormat('contextual/render', 'json', $post, array('query' => array('destination' => 'test-display')));
$this->assertResponse(200);
$json = Json::decode($response);
$this->assertIdentical($json[$id], '<ul class="contextual-links"><li class="entityviewedit-form"><a href="' . base_path() . 'admin/structure/views/view/test_display/edit/page_1">Edit view</a></li></ul>');
// When a "main content" is placed, we still find a contextual link
// placeholder for editing just the view (not the main content block).
// @see system_block_view_system_main_block_alter()
$this->drupalPlaceBlock('system_main_block', ['id' => 'main_content']);
$this->drupalGet('test-display');
$id = 'entity.view.edit_form:view=test_display:location=page&name=test_display&display_id=page_1&langcode=en';
// @see \Drupal\contextual\Tests\ContextualDynamicContextTest:assertContextualLinkPlaceHolder()
$this->assertRaw('<div' . new Attribute(array('data-contextual-id' => $id)) . '></div>', format_string('Contextual link placeholder with id @id exists.', array('@id' => $id)));
}
/**
* Tests that the view status is correctly reflected on the edit form.
*/
public function testViewStatus() {
$view = $this->randomView();
$id = $view['id'];
// The view should initially have the enabled class on it's form wrapper.
$this->drupalGet('admin/structure/views/view/' . $id);
$elements = $this->xpath('//div[contains(@class, :edit) and contains(@class, :status)]', array(':edit' => 'views-edit-view', ':status' => 'enabled'));
$this->assertTrue($elements, 'The enabled class was found on the form wrapper');
$view = Views::getView($id);
$view->storage->disable()->save();
$this->drupalGet('admin/structure/views/view/' . $id);
$elements = $this->xpath('//div[contains(@class, :edit) and contains(@class, :status)]', array(':edit' => 'views-edit-view', ':status' => 'disabled'));
$this->assertTrue($elements, 'The disabled class was found on the form wrapper.');
}
/**
* Ensures that no XSS is possible for buttons.
*/
public function testDisplayTitleInButtonsXss() {
$xss_markup = '"><script>alert(123)</script>';
$view = $this->randomView();
$view = View::load($view['id']);
\Drupal::configFactory()->getEditable('views.settings')->set('ui.show.master_display', TRUE)->save();
foreach ([$xss_markup, '&quot;><script>alert(123)</script>'] as $input) {
$display =& $view->getDisplay('page_1');
$display['display_title'] = $input;
$view->save();
$this->drupalGet("admin/structure/views/view/{$view->id()}");
$escaped = views_ui_truncate($input, 25);
$this->assertEscaped($escaped);
$this->assertNoRaw($xss_markup);
$this->drupalGet("admin/structure/views/view/{$view->id()}/edit/page_1");
$this->assertEscaped("View $escaped");
$this->assertNoRaw("View $xss_markup");
$this->assertEscaped("Duplicate $escaped");
$this->assertNoRaw("Duplicate $xss_markup");
$this->assertEscaped("Delete $escaped");
$this->assertNoRaw("Delete $xss_markup");
}
}
/**
* Tests the action links on the edit display UI.
*/
public function testActionLinks() {
// Change the display title of a display so it contains characters that will
// be escaped when rendered.
$display_title = "'<test>'";
$this->drupalGet('admin/structure/views/view/test_display');
$display_title_path = 'admin/structure/views/nojs/display/test_display/block_1/display_title';
$this->drupalPostForm($display_title_path, array('display_title' => $display_title), t('Apply'));
// Ensure that the title is escaped as expected.
$this->assertEscaped($display_title);
$this->assertNoRaw($display_title);
// Ensure that the dropdown buttons are displayed correctly.
$this->assertFieldByXpath('//input[@type="submit"]', 'Duplicate ' . $display_title);
$this->assertFieldByXpath('//input[@type="submit"]', 'Delete ' . $display_title);
$this->assertFieldByXpath('//input[@type="submit"]', 'Disable ' . $display_title);
$this->assertNoFieldByXpath('//input[@type="submit"]', 'Enable ' . $display_title);
// Disable the display so we can test the rendering of the "Enable" button.
$this->drupalPostForm(NULL, NULL, 'Disable ' . $display_title);
$this->assertFieldByXpath('//input[@type="submit"]', 'Enable ' . $display_title);
$this->assertNoFieldByXpath('//input[@type="submit"]', 'Disable ' . $display_title);
// Ensure that the title is escaped as expected.
$this->assertEscaped($display_title);
$this->assertNoRaw($display_title);
}
/**
* Tests that the override option is hidden when it's not needed.
*/
public function testHideDisplayOverride() {
// Test that the override option appears with two displays.
$this->drupalGet('admin/structure/views/nojs/handler/test_display/page_1/field/title');
$this->assertText('All displays');
// Remove a display and test if the override option is hidden.
$this->drupalPostForm('admin/structure/views/view/test_display/edit/block_1', [], t('Delete @display', ['@display' => 'Block']));
$this->drupalPostForm(NULL, [], t('Save'));
$this->drupalGet('admin/structure/views/nojs/handler/test_display/page_1/field/title');
$this->assertNoText('All displays');
// Test that the override option is shown when display master is on.
\Drupal::configFactory()->getEditable('views.settings')->set('ui.show.master_display', TRUE)->save();
$this->drupalGet('admin/structure/views/nojs/handler/test_display/page_1/field/title');
$this->assertText('All displays');
// Test that the override option is shown if the current display is
// overridden so that the option to revert is available.
$this->drupalPostForm(NULL, ['override[dropdown]' => 'page_1'], t('Apply'));
\Drupal::configFactory()->getEditable('views.settings')->set('ui.show.master_display', FALSE)->save();
$this->drupalGet('admin/structure/views/nojs/handler/test_display/page_1/field/title');
$this->assertText('Revert to default');
}
}

View file

@ -0,0 +1,43 @@
<?php
namespace Drupal\views_ui\Tests;
/**
* Tests the UI for view duplicate tool.
*
* @group views_ui
*/
class DuplicateTest extends UITestBase {
protected function setUp() {
parent::setUp();
$this->drupalPlaceBlock('page_title_block');
}
/**
* Checks if duplicated view exists and has correct label.
*/
public function testDuplicateView() {
// Create random view.
$random_view = $this->randomView();
// Initialize array for duplicated view.
$view = array();
// Generate random label and id for new view.
$view['label'] = $this->randomMachineName(255);
$view['id'] = strtolower($this->randomMachineName(128));
// Duplicate view.
$this->drupalPostForm('admin/structure/views/view/' . $random_view['id'] . '/duplicate', $view, t('Duplicate'));
// Assert that the page url is correct.
$this->assertUrl('admin/structure/views/view/' . $view['id'], array(), 'Make sure the view saving was successful and the browser got redirected to the edit page.');
// Assert that the page title is correctly displayed.
$this->assertText($view['label']);
}
}

View file

@ -0,0 +1,268 @@
<?php
namespace Drupal\views_ui\Tests;
use Drupal\views\Entity\View;
/**
* Tests exposed forms UI functionality.
*
* @group views_ui
*/
class ExposedFormUITest extends UITestBase {
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = array('test_exposed_admin_ui');
/**
* {@inheritdoc}
*/
public static $modules = array('node', 'views_ui', 'block', 'taxonomy', 'field_ui', 'datetime');
/**
* Array of error message strings raised by the grouped form.
*
* @var array
*
* @see FilterPluginBase::buildGroupValidate
*/
protected $groupFormUiErrors = [];
protected function setUp() {
parent::setUp();
$this->drupalCreateContentType(array('type' => 'article'));
$this->drupalCreateContentType(array('type' => 'page'));
// Create some random nodes.
for ($i = 0; $i < 5; $i++) {
$this->drupalCreateNode();
}
// Error strings used in the grouped filter form validation.
$this->groupFormUiErrors['missing_value'] = t('A value is required if the label for this item is defined.');
$this->groupFormUiErrors['missing_title'] = t('A label is required if the value for this item is defined.');
$this->groupFormUiErrors['missing_title_empty_operator'] = t('A label is required for the specified operator.');
}
/**
* Tests the admin interface of exposed filter and sort items.
*/
function testExposedAdminUi() {
$edit = array();
$this->drupalGet('admin/structure/views/nojs/handler/test_exposed_admin_ui/default/filter/type');
// Be sure that the button is called exposed.
$this->helperButtonHasLabel('edit-options-expose-button-button', t('Expose filter'));
// The first time the filter UI is displayed, the operator and the
// value forms should be shown.
$this->assertFieldById('edit-options-operator-in', '', 'Operator In exists');
$this->assertFieldById('edit-options-operator-not-in', '', 'Operator Not In exists');
$this->assertFieldById('edit-options-value-page', '', 'Checkbox for Page exists');
$this->assertFieldById('edit-options-value-article', '', 'Checkbox for Article exists');
// Click the Expose filter button.
$this->drupalPostForm('admin/structure/views/nojs/handler/test_exposed_admin_ui/default/filter/type', $edit, t('Expose filter'));
// Check the label of the expose button.
$this->helperButtonHasLabel('edit-options-expose-button-button', t('Hide filter'));
// After exposing the filter, Operator and Value should be still here.
$this->assertFieldById('edit-options-operator-in', '', 'Operator In exists');
$this->assertFieldById('edit-options-operator-not-in', '', 'Operator Not In exists');
$this->assertFieldById('edit-options-value-page', '', 'Checkbox for Page exists');
$this->assertFieldById('edit-options-value-article', '', 'Checkbox for Article exists');
// Check the validations of the filter handler.
$edit = array();
$edit['options[expose][identifier]'] = '';
$this->drupalPostForm(NULL, $edit, t('Apply'));
$this->assertText(t('The identifier is required if the filter is exposed.'));
$edit = array();
$edit['options[expose][identifier]'] = 'value';
$this->drupalPostForm(NULL, $edit, t('Apply'));
$this->assertText(t('This identifier is not allowed.'));
// Now check the sort criteria.
$this->drupalGet('admin/structure/views/nojs/handler/test_exposed_admin_ui/default/sort/created');
$this->helperButtonHasLabel('edit-options-expose-button-button', t('Expose sort'));
$this->assertNoFieldById('edit-options-expose-label', '', 'Make sure no label field is shown');
// Un-expose the filter.
$this->drupalGet('admin/structure/views/nojs/handler/test_exposed_admin_ui/default/filter/type');
$this->drupalPostForm(NULL, array(), t('Hide filter'));
// After Un-exposing the filter, Operator and Value should be shown again.
$this->assertFieldById('edit-options-operator-in', '', 'Operator In exists after hide filter');
$this->assertFieldById('edit-options-operator-not-in', '', 'Operator Not In exists after hide filter');
$this->assertFieldById('edit-options-value-page', '', 'Checkbox for Page exists after hide filter');
$this->assertFieldById('edit-options-value-article', '', 'Checkbox for Article exists after hide filter');
// Click the Expose sort button.
$edit = array();
$this->drupalPostForm('admin/structure/views/nojs/handler/test_exposed_admin_ui/default/sort/created', $edit, t('Expose sort'));
// Check the label of the expose button.
$this->helperButtonHasLabel('edit-options-expose-button-button', t('Hide sort'));
$this->assertFieldById('edit-options-expose-label', '', 'Make sure a label field is shown');
// Test adding a new exposed sort criteria.
$view_id = $this->randomView()['id'];
$this->drupalGet("admin/structure/views/nojs/add-handler/$view_id/default/sort");
$this->drupalPostForm(NULL, ['name[node_field_data.created]' => 1], t('Add and configure @handler', ['@handler' => t('sort criteria')]));
$this->assertFieldByXPath('//input[@name="options[order]" and @checked="checked"]', 'ASC', 'The default order is set.');
// Change the order and expose the sort.
$this->drupalPostForm(NULL, ['options[order]' => 'DESC'], t('Apply'));
$this->drupalPostForm("admin/structure/views/nojs/handler/$view_id/default/sort/created", [], t('Expose sort'));
$this->assertFieldByXPath('//input[@name="options[order]" and @checked="checked"]', 'DESC');
$this->assertFieldByName('options[expose][label]', 'Authored on', 'The default label is set.');
// Change the label and save the view.
$edit = ['options[expose][label]' => $this->randomString()];
$this->drupalPostForm(NULL, $edit, t('Apply'));
$this->drupalPostForm(NULL, [], t('Save'));
// Check that the values were saved.
$display = View::load($view_id)->getDisplay('default');
$this->assertTrue($display['display_options']['sorts']['created']['exposed']);
$this->assertEqual($display['display_options']['sorts']['created']['expose'], ['label' => $edit['options[expose][label]']]);
$this->assertEqual($display['display_options']['sorts']['created']['order'], 'DESC');
}
/**
* Tests the admin interface of exposed grouped filters.
*/
function testGroupedFilterAdminUi() {
$edit = array();
$this->drupalGet('admin/structure/views/nojs/handler/test_exposed_admin_ui/default/filter/type');
// Click the Expose filter button.
$this->drupalPostForm('admin/structure/views/nojs/handler/test_exposed_admin_ui/default/filter/type', $edit, t('Expose filter'));
// Check the label of the grouped filters button.
$this->helperButtonHasLabel('edit-options-group-button-button', t('Grouped filters'));
// Click the Grouped Filters button.
$this->drupalGet('admin/structure/views/nojs/handler/test_exposed_admin_ui/default/filter/type');
$this->drupalPostForm(NULL, array(), t('Grouped filters'));
// After click on 'Grouped Filters', the standard operator and value should
// not be displayed.
$this->assertNoFieldById('edit-options-operator-in', '', 'Operator In not exists');
$this->assertNoFieldById('edit-options-operator-not-in', '', 'Operator Not In not exists');
$this->assertNoFieldById('edit-options-value-page', '', 'Checkbox for Page not exists');
$this->assertNoFieldById('edit-options-value-article', '', 'Checkbox for Article not exists');
// Check that after click on 'Grouped Filters', a new button is shown to
// add more items to the list.
$this->helperButtonHasLabel('edit-options-group-info-add-group', t('Add another item'));
// Validate a single entry for a grouped filter.
$this->drupalGet('admin/structure/views/nojs/handler/test_exposed_admin_ui/default/filter/type');
$edit = array();
$edit["options[group_info][group_items][1][title]"] = 'Is Article';
$edit["options[group_info][group_items][1][value][article]"] = 'article';
$this->drupalPostForm(NULL, $edit, t('Apply'));
$this->assertUrl('admin/structure/views/view/test_exposed_admin_ui/edit/default');
$this->assertNoGroupedFilterErrors();
// Validate multiple entries for grouped filters.
$this->drupalGet('admin/structure/views/nojs/handler/test_exposed_admin_ui/default/filter/type');
$edit = array();
$edit["options[group_info][group_items][1][title]"] = 'Is Article';
$edit["options[group_info][group_items][1][value][article]"] = 'article';
$edit["options[group_info][group_items][2][title]"] = 'Is Page';
$edit["options[group_info][group_items][2][value][page]"] = 'page';
$edit["options[group_info][group_items][3][title]"] = 'Is Page and Article';
$edit["options[group_info][group_items][3][value][article]"] = 'article';
$edit["options[group_info][group_items][3][value][page]"] = 'page';
$this->drupalPostForm(NULL, $edit, t('Apply'));
$this->assertUrl('admin/structure/views/view/test_exposed_admin_ui/edit/default', array(), 'Correct validation of the node type filter.');
$this->assertNoGroupedFilterErrors();
// Validate an "is empty" filter -- title without value is valid.
$this->drupalGet('admin/structure/views/nojs/handler/test_exposed_admin_ui/default/filter/body_value');
$edit = array();
$edit["options[group_info][group_items][1][title]"] = 'No body';
$edit["options[group_info][group_items][1][operator]"] = 'empty';
$this->drupalPostForm(NULL, $edit, t('Apply'));
$this->assertUrl('admin/structure/views/view/test_exposed_admin_ui/edit/default', array(), 'The "empty" operator validates correctly.');
$this->assertNoGroupedFilterErrors();
// Ensure the string "0" can be used as a value for numeric filters.
$this->drupalPostForm('admin/structure/views/nojs/add-handler/test_exposed_admin_ui/default/filter', array('name[node_field_data.nid]' => TRUE), t('Add and configure @handler', array('@handler' => t('filter criteria'))));
$this->drupalPostForm(NULL, array(), t('Expose filter'));
$this->drupalPostForm(NULL, array(), t('Grouped filters'));
$edit = array();
$edit['options[group_info][group_items][1][title]'] = 'Testing zero';
$edit['options[group_info][group_items][1][operator]'] = '>';
$edit['options[group_info][group_items][1][value][value]'] = '0';
$this->drupalPostForm(NULL, $edit, t('Apply'));
$this->assertUrl('admin/structure/views/view/test_exposed_admin_ui/edit/default', array(), 'A string "0" is a valid value.');
$this->assertNoGroupedFilterErrors();
// Ensure "between" filters validate correctly.
$this->drupalGet('admin/structure/views/nojs/handler/test_exposed_admin_ui/default/filter/nid');
$edit['options[group_info][group_items][1][title]'] = 'ID between test';
$edit['options[group_info][group_items][1][operator]'] = 'between';
$edit['options[group_info][group_items][1][value][min]'] = '0';
$edit['options[group_info][group_items][1][value][max]'] = '10';
$this->drupalPostForm(NULL, $edit, t('Apply'));
$this->assertUrl('admin/structure/views/view/test_exposed_admin_ui/edit/default', array(), 'The "between" filter validates correctly.');
$this->assertNoGroupedFilterErrors();
}
public function testGroupedFilterAdminUiErrors() {
// Select the empty operator without a title specified.
$this->drupalGet('admin/structure/views/nojs/handler/test_exposed_admin_ui/default/filter/body_value');
$edit = array();
$edit["options[group_info][group_items][1][title]"] = '';
$edit["options[group_info][group_items][1][operator]"] = 'empty';
$this->drupalPostForm(NULL, $edit, t('Apply'));
$this->assertText($this->groupFormUiErrors['missing_title_empty_operator']);
// Specify a title without a value.
$this->drupalGet('admin/structure/views/nojs/handler/test_exposed_admin_ui/default/filter/type');
$this->drupalPostForm('admin/structure/views/nojs/handler/test_exposed_admin_ui/default/filter/type', [], t('Expose filter'));
$this->drupalPostForm('admin/structure/views/nojs/handler/test_exposed_admin_ui/default/filter/type', [], t('Grouped filters'));
$edit = array();
$edit["options[group_info][group_items][1][title]"] = 'Is Article';
$this->drupalPostForm(NULL, $edit, t('Apply'));
$this->assertText($this->groupFormUiErrors['missing_value']);
// Specify a value without a title.
$this->drupalGet('admin/structure/views/nojs/handler/test_exposed_admin_ui/default/filter/type');
$edit = array();
$edit["options[group_info][group_items][1][title]"] = '';
$edit["options[group_info][group_items][1][value][article]"] = 'article';
$this->drupalPostForm(NULL, $edit, t('Apply'));
$this->assertText($this->groupFormUiErrors['missing_title']);
}
/**
* Asserts that there are no Grouped Filters errors.
*
* @param string $message
* The assert message.
* @param string $group
* The assertion group.
*
* @return bool
* Result of the assertion.
*/
protected function assertNoGroupedFilterErrors($message = '', $group = 'Other') {
foreach ($this->groupFormUiErrors as $error) {
$err_message = $message;
if (empty($err_message)) {
$err_message = "Verify that '$error' is not in the HTML output.";
}
if (empty($message)) {
return $this->assertNoRaw($error, $err_message, $group);
}
}
return TRUE;
}
}

View file

@ -0,0 +1,100 @@
<?php
namespace Drupal\views_ui\Tests;
use Drupal\Component\Serialization\Json;
use Drupal\views\Views;
/**
* Tests the UI of field handlers.
*
* @group views_ui
* @see \Drupal\views\Plugin\views\field\FieldPluginBase
*/
class FieldUITest extends UITestBase {
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = array('test_view');
/**
* Tests the UI of field handlers.
*/
public function testFieldUI() {
// Ensure the field is not marked as hidden on the first run.
$this->drupalGet('admin/structure/views/view/test_view/edit');
$this->assertText('Views test: Name');
$this->assertNoText('Views test: Name [' . t('hidden') . ']');
// Hides the field and check whether the hidden label is appended.
$edit_handler_url = 'admin/structure/views/nojs/handler/test_view/default/field/name';
$this->drupalPostForm($edit_handler_url, array('options[exclude]' => TRUE), t('Apply'));
$this->assertText('Views test: Name [' . t('hidden') . ']');
// Ensure that the expected tokens appear in the UI.
$edit_handler_url = 'admin/structure/views/nojs/handler/test_view/default/field/age';
$this->drupalGet($edit_handler_url);
$result = $this->xpath('//details[@id="edit-options-alter-help"]/div[@class="details-wrapper"]/div[@class="item-list"]/ul/li');
$this->assertEqual((string) $result[0], '{{ age }} == Age');
$edit_handler_url = 'admin/structure/views/nojs/handler/test_view/default/field/id';
$this->drupalGet($edit_handler_url);
$result = $this->xpath('//details[@id="edit-options-alter-help"]/div[@class="details-wrapper"]/div[@class="item-list"]/ul/li');
$this->assertEqual((string) $result[0], '{{ age }} == Age');
$this->assertEqual((string) $result[1], '{{ id }} == ID');
$edit_handler_url = 'admin/structure/views/nojs/handler/test_view/default/field/name';
$this->drupalGet($edit_handler_url);
$result = $this->xpath('//details[@id="edit-options-alter-help"]/div[@class="details-wrapper"]/div[@class="item-list"]/ul/li');
$this->assertEqual((string) $result[0], '{{ age }} == Age');
$this->assertEqual((string) $result[1], '{{ id }} == ID');
$this->assertEqual((string) $result[2], '{{ name }} == Name');
$result = $this->xpath('//details[@id="edit-options-more"]');
$this->assertEqual(empty($result), TRUE, "Container 'more' is empty and should not be displayed.");
// Ensure that dialog titles are not escaped.
$edit_groupby_url = 'admin/structure/views/nojs/handler/test_view/default/field/name';
$this->assertNoLinkByHref($edit_groupby_url, 0, 'No aggregation link found.');
// Enable aggregation on the view.
$edit = array(
'group_by' => TRUE,
);
$this->drupalPostForm('/admin/structure/views/nojs/display/test_view/default/group_by', $edit, t('Apply'));
$this->assertLinkByHref($edit_groupby_url, 0, 'Aggregation link found.');
$edit_handler_url = '/admin/structure/views/ajax/handler-group/test_view/default/field/name';
$this->drupalGet($edit_handler_url);
$data = Json::decode($this->getRawContent());
$this->assertEqual($data[3]['dialogOptions']['title'], 'Configure aggregation settings for field Views test: Name');
}
/**
* Tests the field labels.
*/
public function testFieldLabel() {
// Create a view with unformatted style and make sure the fields have no
// labels by default.
$view = array();
$view['label'] = $this->randomMachineName(16);
$view['id'] = strtolower($this->randomMachineName(16));
$view['description'] = $this->randomMachineName(16);
$view['show[wizard_key]'] = 'node';
$view['page[create]'] = TRUE;
$view['page[style][style_plugin]'] = 'default';
$view['page[title]'] = $this->randomMachineName(16);
$view['page[path]'] = $view['id'];
$this->drupalPostForm('admin/structure/views/add', $view, t('Save and edit'));
$view = Views::getView($view['id']);
$view->initHandlers();
$this->assertEqual($view->field['title']->options['label'], '', 'The field label for normal styles are empty.');
}
}

View file

@ -0,0 +1,68 @@
<?php
namespace Drupal\views_ui\Tests;
/**
* Tests the boolean filter UI.
*
* @group views_ui
* @see \Drupal\views\Plugin\views\filter\BooleanOperator
*/
class FilterBooleanWebTest extends UITestBase {
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = array('test_view');
/**
* Tests the filter boolean UI.
*/
public function testFilterBooleanUI() {
$this->drupalPostForm('admin/structure/views/nojs/add-handler/test_view/default/filter', array('name[views_test_data.status]' => TRUE), t('Add and configure @handler', array('@handler' => t('filter criteria'))));
// Check the field widget label. 'title' should be used as a fallback.
$result = $this->cssSelect('#edit-options-value--wrapper legend span');
$this->assertEqual((string) $result[0], 'Status');
$this->drupalPostForm(NULL, array(), t('Expose filter'));
$this->drupalPostForm(NULL, array(), t('Grouped filters'));
$edit = array();
$edit['options[group_info][group_items][1][title]'] = 'Published';
$edit['options[group_info][group_items][1][operator]'] = '=';
$edit['options[group_info][group_items][1][value]'] = 1;
$edit['options[group_info][group_items][2][title]'] = 'Not published';
$edit['options[group_info][group_items][2][operator]'] = '=';
$edit['options[group_info][group_items][2][value]'] = 0;
$edit['options[group_info][group_items][3][title]'] = 'Not published2';
$edit['options[group_info][group_items][3][operator]'] = '!=';
$edit['options[group_info][group_items][3][value]'] = 1;
$this->drupalPostForm(NULL, $edit, t('Apply'));
$this->drupalGet('admin/structure/views/nojs/handler/test_view/default/filter/status');
$result = $this->xpath('//input[@name="options[group_info][group_items][1][value]"]');
$this->assertEqual((int) $result[1]->attributes()->checked, 'checked');
$result = $this->xpath('//input[@name="options[group_info][group_items][2][value]"]');
$this->assertEqual((int) $result[2]->attributes()->checked, 'checked');
$result = $this->xpath('//input[@name="options[group_info][group_items][3][value]"]');
$this->assertEqual((int) $result[1]->attributes()->checked, 'checked');
// Test that there is a remove link for each group.
$this->assertEqual(count($this->cssSelect('a.views-remove-link')), 3);
// Test selecting a default and removing an item.
$edit = array();
$edit['options[group_info][default_group]'] = 2;
$edit['options[group_info][group_items][3][remove]'] = 1;
$this->drupalPostForm(NULL, $edit, t('Apply'));
$this->drupalGet('admin/structure/views/nojs/handler/test_view/default/filter/status');
$this->assertFieldByName('options[group_info][default_group]', 2, 'Second item was set as the default.');
$this->assertNoField('options[group_info][group_items][3][remove]', 'Third item was removed.');
}
}

View file

@ -0,0 +1,112 @@
<?php
namespace Drupal\views_ui\Tests;
use Drupal\config\Tests\SchemaCheckTestTrait;
/**
* Tests the numeric filter UI.
*
* @group views_ui
* @see \Drupal\views\Plugin\views\filter\NumericFilter
*/
class FilterNumericWebTest extends UITestBase {
use SchemaCheckTestTrait;
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = array('test_view');
/**
* Tests the filter numeric UI.
*/
public function testFilterNumericUI() {
$this->drupalPostForm('admin/structure/views/nojs/add-handler/test_view/default/filter', array('name[views_test_data.age]' => TRUE), t('Add and configure @handler', array('@handler' => t('filter criteria'))));
$this->drupalPostForm(NULL, array(), t('Expose filter'));
$this->drupalPostForm(NULL, array(), t('Grouped filters'));
$edit = array();
$edit['options[group_info][group_items][1][title]'] = 'Old';
$edit['options[group_info][group_items][1][operator]'] = '>';
$edit['options[group_info][group_items][1][value][value]'] = 27;
$edit['options[group_info][group_items][2][title]'] = 'Young';
$edit['options[group_info][group_items][2][operator]'] = '<=';
$edit['options[group_info][group_items][2][value][value]'] = 27;
$edit['options[group_info][group_items][3][title]'] = 'From 26 to 28';
$edit['options[group_info][group_items][3][operator]'] = 'between';
$edit['options[group_info][group_items][3][value][min]'] = 26;
$edit['options[group_info][group_items][3][value][max]'] = 28;
$this->drupalPostForm(NULL, $edit, t('Apply'));
$this->drupalGet('admin/structure/views/nojs/handler/test_view/default/filter/age');
foreach ($edit as $name => $value) {
$this->assertFieldByName($name, $value);
}
$this->drupalPostForm('admin/structure/views/view/test_view', array(), t('Save'));
$this->assertConfigSchemaByName('views.view.test_view');
// Test that the exposed filter works as expected.
$this->drupalPostForm(NULL, array(), t('Update preview'));
$this->assertText('John');
$this->assertText('Paul');
$this->assertText('Ringo');
$this->assertText('George');
$this->assertText('Meredith');
$this->drupalPostForm(NULL, array('age' => '2'), t('Update preview'));
$this->assertText('John');
$this->assertText('Paul');
$this->assertNoText('Ringo');
$this->assertText('George');
$this->assertNoText('Meredith');
// Change the filter to a single filter to test the schema when the operator
// is not exposed.
$this->drupalPostForm('admin/structure/views/nojs/handler/test_view/default/filter/age', array(), t('Single filter'));
$edit = array();
$edit['options[value][value]'] = 25;
$this->drupalPostForm(NULL, $edit, t('Apply'));
$this->drupalPostForm('admin/structure/views/view/test_view', array(), t('Save'));
$this->assertConfigSchemaByName('views.view.test_view');
// Test that the filter works as expected.
$this->drupalPostForm(NULL, array(), t('Update preview'));
$this->assertText('John');
$this->assertNoText('Paul');
$this->assertNoText('Ringo');
$this->assertNoText('George');
$this->assertNoText('Meredith');
$this->drupalPostForm(NULL, array('age' => '26'), t('Update preview'));
$this->assertNoText('John');
$this->assertText('Paul');
$this->assertNoText('Ringo');
$this->assertNoText('George');
$this->assertNoText('Meredith');
// Change the filter to a 'between' filter to test if the label and
// description are set for the 'minimum' filter element.
$this->drupalGet('admin/structure/views/nojs/handler/test_view/default/filter/age');
$edit = array();
$edit['options[expose][label]'] = 'Age between';
$edit['options[expose][description]'] = 'Description of the exposed filter';
$edit['options[operator]'] = 'between';
$edit['options[value][min]'] = 26;
$edit['options[value][max]'] = 28;
$this->drupalPostForm(NULL, $edit, t('Apply'));
$this->drupalPostForm('admin/structure/views/view/test_view', array(), t('Save'));
$this->assertConfigSchemaByName('views.view.test_view');
$this->drupalPostForm(NULL, array(), t('Update preview'));
// Check the max field label.
$this->assertRaw('<label for="edit-age-max">And</label>', 'Max field label found');
$this->assertRaw('<label for="edit-age-min">Age between</label>', 'Min field label found');
// Check that the description is shown in the right place.
$this->assertEqual(trim($this->cssSelect('.form-item-age-min .description')[0]), 'Description of the exposed filter');
}
}

View file

@ -0,0 +1,123 @@
<?php
namespace Drupal\views_ui\Tests;
use Drupal\views\Tests\ViewTestBase;
/**
* Tests for the filters from the UI.
*
* @group views_ui
*/
class FilterUITest extends ViewTestBase {
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = array('test_filter_in_operator_ui', 'test_filter_groups');
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('views_ui', 'node');
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->drupalCreateContentType(array('type' => 'page'));
$this->enableViewsTestModule();
}
/**
* Tests that an option for a filter is saved as expected from the UI.
*/
public function testFilterInOperatorUi() {
$admin_user = $this->drupalCreateUser(array('administer views', 'administer site configuration'));
$this->drupalLogin($admin_user);
$path = 'admin/structure/views/nojs/handler/test_filter_in_operator_ui/default/filter/type';
$this->drupalGet($path);
// Verifies that "Limit list to selected items" option is not selected.
$this->assertFieldByName('options[expose][reduce]', FALSE);
// Select "Limit list to selected items" option and apply.
$edit = array(
'options[expose][reduce]' => TRUE,
);
$this->drupalPostForm($path, $edit, t('Apply'));
// Verifies that the option was saved as expected.
$this->drupalGet($path);
$this->assertFieldByName('options[expose][reduce]', TRUE);
}
/**
* Tests the filters from the UI.
*/
public function testFiltersUI() {
$admin_user = $this->drupalCreateUser(array('administer views', 'administer site configuration'));
$this->drupalLogin($admin_user);
$this->drupalGet('admin/structure/views/view/test_filter_groups');
$this->assertLink('Content: ID (= 1)', 0, 'Content: ID (= 1) link appears correctly.');
// Tests that we can create a new filter group from UI.
$this->drupalGet('admin/structure/views/nojs/rearrange-filter/test_filter_groups/page');
$this->assertNoRaw('<span>Group 3</span>', 'Group 3 has not been added yet.');
// Create 2 new groups.
$this->drupalPostForm(NULL, [], t('Create new filter group'));
$this->drupalPostForm(NULL, [], t('Create new filter group'));
// Remove the new group 3.
$this->drupalPostForm(NULL, [], t('Remove group 3'));
// Verify that the group 4 is now named as 3.
$this->assertRaw('<span>Group 3</span>', 'Group 3 still exists.');
// Remove the group 3 again.
$this->drupalPostForm(NULL, [], t('Remove group 3'));
// Group 3 now does not exist.
$this->assertNoRaw('<span>Group 3</span>', 'Group 3 has not been added yet.');
}
/**
* Tests the identifier settings and restrictions.
*/
public function testFilterIdentifier() {
$admin_user = $this->drupalCreateUser(array('administer views', 'administer site configuration'));
$this->drupalLogin($admin_user);
$path = 'admin/structure/views/nojs/handler/test_filter_in_operator_ui/default/filter/type';
// Set an empty identifier.
$edit = array(
'options[expose][identifier]' => '',
);
$this->drupalPostForm($path, $edit, t('Apply'));
$this->assertText('The identifier is required if the filter is exposed.');
// Set the identifier to 'value'.
$edit = array(
'options[expose][identifier]' => 'value',
);
$this->drupalPostForm($path, $edit, t('Apply'));
$this->assertText('This identifier is not allowed.');
// Set the identifier to a value with a restricted character.
$edit = array(
'options[expose][identifier]' => 'value value',
);
$this->drupalPostForm($path, $edit, t('Apply'));
$this->assertText('This identifier has illegal characters.');
}
}

View file

@ -0,0 +1,50 @@
<?php
namespace Drupal\views_ui\Tests;
/**
* Tests UI of aggregate functionality..
*
* @group views_ui
*/
class GroupByTest extends UITestBase {
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = array('test_views_groupby_save');
/**
* Tests whether basic saving works.
*
* @todo This should check the change of the settings as well.
*/
function testGroupBySave() {
$this->drupalGet('admin/structure/views/view/test_views_groupby_save/edit');
$edit_groupby_url = 'admin/structure/views/nojs/handler-group/test_views_groupby_save/default/field/id';
$this->assertNoLinkByHref($edit_groupby_url, 0, 'No aggregation link found.');
// Enable aggregation on the view.
$edit = array(
'group_by' => TRUE,
);
$this->drupalPostForm('admin/structure/views/nojs/display/test_views_groupby_save/default/group_by', $edit, t('Apply'));
$this->assertLinkByHref($edit_groupby_url, 0, 'Aggregation link found.');
// Change the groupby type in the UI.
$this->drupalPostForm($edit_groupby_url, array('options[group_type]' => 'count'), t('Apply'));
$this->assertLink('COUNT(Views test: ID)', 0, 'The count setting is displayed in the UI');
$this->drupalPostForm(NULL, array(), t('Save'));
$view = $this->container->get('entity.manager')->getStorage('view')->load('test_views_groupby_save');
$display = $view->getDisplay('default');
$this->assertTrue($display['display_options']['group_by'], 'The groupby setting was saved on the view.');
$this->assertEqual($display['display_options']['fields']['id']['group_type'], 'count', 'Count groupby_type was saved on the view.');
}
}

View file

@ -0,0 +1,282 @@
<?php
namespace Drupal\views_ui\Tests;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\views\Tests\ViewTestData;
use Drupal\views\ViewExecutable;
/**
* Tests handler UI for views.
*
* @group views_ui
* @see \Drupal\views\Plugin\views\HandlerBase
*/
class HandlerTest extends UITestBase {
/**
* {@inheritdoc}
*/
public static $modules = array('node_test_views');
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = array('test_view_empty', 'test_view_broken', 'node', 'test_node_view');
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->drupalPlaceBlock('page_title_block');
ViewTestData::createTestViews(get_class($this), array('node_test_views'));
}
/**
* Overrides \Drupal\views\Tests\ViewTestBase::schemaDefinition().
*
* Adds a uid column to test the relationships.
*/
protected function schemaDefinition() {
$schema = parent::schemaDefinition();
$schema['views_test_data']['fields']['uid'] = array(
'description' => "The {users}.uid of the author of the beatle entry.",
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0
);
return $schema;
}
/**
* Overrides \Drupal\views\Tests\ViewTestBase::viewsData().
*
* Adds:
* - a relationship for the uid column.
* - a dummy field with no help text.
*/
protected function viewsData() {
$data = parent::viewsData();
$data['views_test_data']['uid'] = array(
'title' => t('UID'),
'help' => t('The test data UID'),
'relationship' => array(
'id' => 'standard',
'base' => 'users_field_data',
'base field' => 'uid'
)
);
// Create a dummy field with no help text.
$data['views_test_data']['no_help'] = $data['views_test_data']['name'];
$data['views_test_data']['no_help']['field']['title'] = t('No help');
$data['views_test_data']['no_help']['field']['real field'] = 'name';
unset($data['views_test_data']['no_help']['help']);
return $data;
}
/**
* Tests UI CRUD.
*/
public function testUICRUD() {
$handler_types = ViewExecutable::getHandlerTypes();
foreach ($handler_types as $type => $type_info) {
// Test adding handlers.
$add_handler_url = "admin/structure/views/nojs/add-handler/test_view_empty/default/$type";
// Area handler types need to use a different handler.
if (in_array($type, array('header', 'footer', 'empty'))) {
$this->drupalPostForm($add_handler_url, array('name[views.area]' => TRUE), t('Add and configure @handler', array('@handler' => $type_info['ltitle'])));
$id = 'area';
$edit_handler_url = "admin/structure/views/nojs/handler/test_view_empty/default/$type/$id";
}
elseif ($type == 'relationship') {
$this->drupalPostForm($add_handler_url, array('name[views_test_data.uid]' => TRUE), t('Add and configure @handler', array('@handler' => $type_info['ltitle'])));
$id = 'uid';
$edit_handler_url = "admin/structure/views/nojs/handler/test_view_empty/default/$type/$id";
}
else {
$this->drupalPostForm($add_handler_url, array('name[views_test_data.job]' => TRUE), t('Add and configure @handler', array('@handler' => $type_info['ltitle'])));
$id = 'job';
$edit_handler_url = "admin/structure/views/nojs/handler/test_view_empty/default/$type/$id";
}
$this->assertUrl($edit_handler_url, array(), 'The user got redirected to the handler edit form.');
$random_label = $this->randomMachineName();
$this->drupalPostForm(NULL, array('options[admin_label]' => $random_label), t('Apply'));
$this->assertUrl('admin/structure/views/view/test_view_empty/edit/default', array(), 'The user got redirected to the views edit form.');
$this->assertLinkByHref($edit_handler_url, 0, 'The handler edit link appears in the UI.');
$links = $this->xpath('//a[starts-with(normalize-space(text()), :label)]', array(':label' => $random_label));
$this->assertTrue(isset($links[0]), 'The handler edit link has the right label');
// Save the view and have a look whether the handler was added as expected.
$this->drupalPostForm(NULL, array(), t('Save'));
$view = $this->container->get('entity.manager')->getStorage('view')->load('test_view_empty');
$display = $view->getDisplay('default');
$this->assertTrue(isset($display['display_options'][$type_info['plural']][$id]), 'Ensure the field was added to the view itself.');
// Remove the item and check that it's removed
$this->drupalPostForm($edit_handler_url, array(), t('Remove'));
$this->assertNoLinkByHref($edit_handler_url, 0, 'The handler edit link does not appears in the UI after removing.');
$this->drupalPostForm(NULL, array(), t('Save'));
$view = $this->container->get('entity.manager')->getStorage('view')->load('test_view_empty');
$display = $view->getDisplay('default');
$this->assertFalse(isset($display['display_options'][$type_info['plural']][$id]), 'Ensure the field was removed from the view itself.');
}
// Test adding a field of the user table using the uid relationship.
$type_info = $handler_types['relationship'];
$add_handler_url = "admin/structure/views/nojs/add-handler/test_view_empty/default/relationship";
$this->drupalPostForm($add_handler_url, array('name[views_test_data.uid]' => TRUE), t('Add and configure @handler', array('@handler' => $type_info['ltitle'])));
$add_handler_url = "admin/structure/views/nojs/add-handler/test_view_empty/default/field";
$type_info = $handler_types['field'];
$this->drupalPostForm($add_handler_url, array('name[users_field_data.name]' => TRUE), t('Add and configure @handler', array('@handler' => $type_info['ltitle'])));
$id = 'name';
$edit_handler_url = "admin/structure/views/nojs/handler/test_view_empty/default/field/$id";
$this->assertUrl($edit_handler_url, array(), 'The user got redirected to the handler edit form.');
$this->assertFieldByName('options[relationship]', 'uid', 'Ensure the relationship select is filled with the UID relationship.');
$this->drupalPostForm(NULL, array(), t('Apply'));
$this->drupalPostForm(NULL, array(), t('Save'));
$view = $this->container->get('entity.manager')->getStorage('view')->load('test_view_empty');
$display = $view->getDisplay('default');
$this->assertTrue(isset($display['display_options'][$type_info['plural']][$id]), 'Ensure the field was added to the view itself.');
}
/**
* Tests escaping of field labels in help text.
*/
public function testHandlerHelpEscaping() {
// Setup a field with two instances using a different label.
// Ensure that the label is escaped properly.
$this->drupalCreateContentType(['type' => 'article']);
$this->drupalCreateContentType(['type' => 'page']);
FieldStorageConfig::create([
'field_name' => 'field_test',
'entity_type' => 'node',
'type' => 'string',
])->save();
FieldConfig::create([
'field_name' => 'field_test',
'entity_type' => 'node',
'bundle' => 'page',
'label' => 'The giraffe" label'
])->save();
FieldConfig::create([
'field_name' => 'field_test',
'entity_type' => 'node',
'bundle' => 'article',
'label' => 'The <em>giraffe"</em> label <script>alert("the return of the xss")</script>'
])->save();
$this->drupalGet('admin/structure/views/nojs/add-handler/content/default/field');
$this->assertEscaped('The <em>giraffe"</em> label <script>alert("the return of the xss")</script>');
$this->assertEscaped('Appears in: page, article. Also known as: Content: The giraffe" label');
}
/**
* Tests broken handlers.
*/
public function testBrokenHandlers() {
$handler_types = ViewExecutable::getHandlerTypes();
foreach ($handler_types as $type => $type_info) {
$this->drupalGet('admin/structure/views/view/test_view_broken/edit');
$href = "admin/structure/views/nojs/handler/test_view_broken/default/$type/id_broken";
$result = $this->xpath('//a[contains(@href, :href)]', array(':href' => $href));
$this->assertEqual(count($result), 1, SafeMarkup::format('Handler (%type) edit link found.', array('%type' => $type)));
$text = 'Broken/missing handler';
$this->assertIdentical((string) $result[0], $text, 'Ensure the broken handler text was found.');
$this->drupalGet($href);
$result = $this->xpath('//h1[@class="page-title"]');
$this->assertTrue(strpos((string) $result[0], $text) !== FALSE, 'Ensure the broken handler text was found.');
$original_configuration = [
'field' => 'id_broken',
'id' => 'id_broken',
'relationship' => 'none',
'table' => 'views_test_data',
'plugin_id' => 'numeric',
];
foreach ($original_configuration as $key => $value) {
$this->assertText(SafeMarkup::format('@key: @value', array('@key' => $key, '@value' => $value)));
}
}
}
/**
* Ensures that neither node type or node ID appears multiple times.
*
* @see \Drupal\views\EntityViewsData
*/
public function testNoDuplicateFields() {
$handler_types = ['field', 'filter', 'sort', 'argument'];
foreach ($handler_types as $handler_type) {
$add_handler_url = 'admin/structure/views/nojs/add-handler/test_node_view/default/' . $handler_type;
$this->drupalGet($add_handler_url);
$this->assertNoDuplicateField('ID', 'Content');
$this->assertNoDuplicateField('ID', 'Content revision');
$this->assertNoDuplicateField('Content type', 'Content');
$this->assertNoDuplicateField('UUID', 'Content');
$this->assertNoDuplicateField('Revision ID', 'Content');
$this->assertNoDuplicateField('Revision ID', 'Content revision');
}
}
/**
* Ensures that no missing help text is shown.
*
* @see \Drupal\views\EntityViewsData
*/
public function testErrorMissingHelp() {
// Test that the error message is not shown for entity fields but an empty
// description field is shown instead.
$this->drupalGet('admin/structure/views/nojs/add-handler/test_node_view/default/field');
$this->assertNoText('Error: missing help');
$this->assertRaw('<td class="description"></td>', 'Empty description found');
// Test that no error message is shown for other fields.
$this->drupalGet('admin/structure/views/nojs/add-handler/test_view_empty/default/field');
$this->assertNoText('Error: missing help');
}
/**
* Asserts that fields only appear once.
*
* @param string $field_name
* The field name.
* @param string $entity_type
* The entity type to which the field belongs.
*/
public function assertNoDuplicateField($field_name, $entity_type) {
$elements = $this->xpath('//td[.=:entity_type]/preceding-sibling::td[@class="title" and .=:title]', [':title' => $field_name, ':entity_type' => $entity_type]);
$this->assertEqual(1, count($elements), $field_name . ' appears just once in ' . $entity_type . '.');
}
}

View file

@ -0,0 +1,51 @@
<?php
namespace Drupal\views_ui\Tests;
use Drupal\simpletest\WebTestBase;
/**
* Tests configuration schema against new views.
*
* @group views_ui
*/
class NewViewConfigSchemaTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('views_ui', 'node', 'comment', 'file', 'taxonomy', 'dblog', 'aggregator');
/**
* Tests creating brand new views.
*/
public function testNewViews() {
$this->drupalLogin($this->drupalCreateUser(array('administer views')));
// Create views with all core Views wizards.
$wizards = array(
// Wizard with their own classes.
'node',
'node_revision',
'users',
'comment',
'file_managed',
'taxonomy_term',
'watchdog',
// Standard derivative classes.
'standard:aggregator_feed',
'standard:aggregator_item',
);
foreach ($wizards as $wizard_key) {
$edit = array();
$edit['label'] = $this->randomString();
$edit['id'] = strtolower($this->randomMachineName());
$edit['show[wizard_key]'] = $wizard_key;
$edit['description'] = $this->randomString();
$this->drupalPostForm('admin/structure/views/add', $edit, t('Save and edit'));
}
}
}

View file

@ -0,0 +1,199 @@
<?php
namespace Drupal\views_ui\Tests;
/**
* Tests that displays can be correctly overridden via the user interface.
*
* @group views_ui
*/
class OverrideDisplaysTest extends UITestBase {
protected function setUp() {
parent::setUp();
$this->drupalPlaceBlock('page_title_block');
}
/**
* Tests that displays can be overridden via the UI.
*/
function testOverrideDisplays() {
// Create a basic view that shows all content, with a page and a block
// display.
$view['label'] = $this->randomMachineName(16);
$view['id'] = strtolower($this->randomMachineName(16));
$view['page[create]'] = 1;
$view['page[path]'] = $this->randomMachineName(16);
$view['block[create]'] = 1;
$view_path = $view['page[path]'];
$this->drupalPostForm('admin/structure/views/add', $view, t('Save and edit'));
// Configure its title. Since the page and block both started off with the
// same (empty) title in the views wizard, we expect the wizard to have set
// things up so that they both inherit from the default display, and we
// therefore only need to change that to have it take effect for both.
$edit = array();
$edit['title'] = $original_title = $this->randomMachineName(16);
$edit['override[dropdown]'] = 'default';
$this->drupalPostForm("admin/structure/views/nojs/display/{$view['id']}/page_1/title", $edit, t('Apply'));
$this->drupalPostForm("admin/structure/views/view/{$view['id']}/edit/page_1", array(), t('Save'));
// Add a node that will appear in the view, so that the block will actually
// be displayed.
$this->drupalCreateContentType(array('type' => 'page'));
$this->drupalCreateNode();
// Make sure the title appears in the page.
$this->drupalGet($view_path);
$this->assertResponse(200);
$this->assertText($original_title);
// Confirm that the view block is available in the block administration UI.
$this->drupalGet('admin/structure/block/list/' . $this->config('system.theme')->get('default'));
$this->clickLinkPartialName('Place block');
$this->assertText($view['label']);
// Place the block.
$this->drupalPlaceBlock("views_block:{$view['id']}-block_1");
// Make sure the title appears in the block.
$this->drupalGet('');
$this->assertText($original_title);
// Change the title for the page display only, and make sure that the
// original title still appears on the page.
$edit = array();
$edit['title'] = $new_title = $this->randomMachineName(16);
$edit['override[dropdown]'] = 'page_1';
$this->drupalPostForm("admin/structure/views/nojs/display/{$view['id']}/page_1/title", $edit, t('Apply'));
$this->drupalPostForm("admin/structure/views/view/{$view['id']}/edit/page_1", array(), t('Save'));
$this->drupalGet($view_path);
$this->assertResponse(200);
$this->assertText($new_title);
$this->assertText($original_title);
}
/**
* Tests that the wizard correctly sets up default and overridden displays.
*/
function testWizardMixedDefaultOverriddenDisplays() {
// Create a basic view with a page, block, and feed. Give the page and feed
// identical titles, but give the block a different one, so we expect the
// page and feed to inherit their titles from the default display, but the
// block to override it.
$view['label'] = $this->randomMachineName(16);
$view['id'] = strtolower($this->randomMachineName(16));
$view['page[create]'] = 1;
$view['page[title]'] = $this->randomMachineName(16);
$view['page[path]'] = $this->randomMachineName(16);
$view['page[feed]'] = 1;
$view['page[feed_properties][path]'] = $this->randomMachineName(16);
$view['block[create]'] = 1;
$view['block[title]'] = $this->randomMachineName(16);
$this->drupalPostForm('admin/structure/views/add', $view, t('Save and edit'));
// Add a node that will appear in the view, so that the block will actually
// be displayed.
$this->drupalCreateContentType(array('type' => 'page'));
$this->drupalCreateNode();
// Make sure that the feed, page and block all start off with the correct
// titles.
$this->drupalGet($view['page[path]']);
$this->assertResponse(200);
$this->assertText($view['page[title]']);
$this->assertNoText($view['block[title]']);
$this->drupalGet($view['page[feed_properties][path]']);
$this->assertResponse(200);
$this->assertText($view['page[title]']);
$this->assertNoText($view['block[title]']);
// Confirm that the block is available in the block administration UI.
$this->drupalGet('admin/structure/block/list/' . $this->config('system.theme')->get('default'));
$this->clickLinkPartialName('Place block');
$this->assertText($view['label']);
// Put the block into the first sidebar region, and make sure it will not
// display on the view's page display (since we will be searching for the
// presence/absence of the view's title in both the page and the block).
$this->drupalPlaceBlock("views_block:{$view['id']}-block_1", array(
'visibility' => array(
'request_path' => array(
'pages' => '/' . $view['page[path]'],
'negate' => TRUE,
),
),
));
$this->drupalGet('');
$this->assertText($view['block[title]']);
$this->assertNoText($view['page[title]']);
// Edit the page and change the title. This should automatically change
// the feed's title also, but not the block.
$edit = array();
$edit['title'] = $new_default_title = $this->randomMachineName(16);
$this->drupalPostForm("admin/structure/views/nojs/display/{$view['id']}/page_1/title", $edit, t('Apply'));
$this->drupalPostForm("admin/structure/views/view/{$view['id']}/edit/page_1", array(), t('Save'));
$this->drupalGet($view['page[path]']);
$this->assertResponse(200);
$this->assertText($new_default_title);
$this->assertNoText($view['page[title]']);
$this->assertNoText($view['block[title]']);
$this->drupalGet($view['page[feed_properties][path]']);
$this->assertResponse(200);
$this->assertText($new_default_title);
$this->assertNoText($view['page[title]']);
$this->assertNoText($view['block[title]']);
$this->drupalGet('');
$this->assertNoText($new_default_title);
$this->assertNoText($view['page[title]']);
$this->assertText($view['block[title]']);
// Edit the block and change the title. This should automatically change
// the block title only, and leave the defaults alone.
$edit = array();
$edit['title'] = $new_block_title = $this->randomMachineName(16);
$this->drupalPostForm("admin/structure/views/nojs/display/{$view['id']}/block_1/title", $edit, t('Apply'));
$this->drupalPostForm("admin/structure/views/view/{$view['id']}/edit/block_1", array(), t('Save'));
$this->drupalGet($view['page[path]']);
$this->assertResponse(200);
$this->assertText($new_default_title);
$this->drupalGet($view['page[feed_properties][path]']);
$this->assertResponse(200);
$this->assertText($new_default_title);
$this->assertNoText($new_block_title);
$this->drupalGet('');
$this->assertText($new_block_title);
$this->assertNoText($view['block[title]']);
}
/**
* Tests that the revert to all displays select-option works as expected.
*/
function testRevertAllDisplays() {
// Create a basic view with a page, block.
// Because there is both a title on page and block we expect the title on
// the block be overridden.
$view['label'] = $this->randomMachineName(16);
$view['id'] = strtolower($this->randomMachineName(16));
$view['page[create]'] = 1;
$view['page[title]'] = $this->randomMachineName(16);
$view['page[path]'] = $this->randomMachineName(16);
$view['block[create]'] = 1;
$view['block[title]'] = $this->randomMachineName(16);
$this->drupalPostForm('admin/structure/views/add', $view, t('Save and edit'));
// Revert the title of the block to the default ones, but submit some new
// values to be sure that the new value is not stored.
$edit = array();
$edit['title'] = $new_block_title = $this->randomMachineName();
$edit['override[dropdown]'] = 'default_revert';
$this->drupalPostForm("admin/structure/views/nojs/display/{$view['id']}/block_1/title", $edit, t('Apply'));
$this->drupalPostForm("admin/structure/views/view/{$view['id']}/edit/block_1", array(), t('Save'));
$this->assertText($view['page[title]']);
}
}

View file

@ -0,0 +1,374 @@
<?php
namespace Drupal\views_ui\Tests;
use Drupal\Component\Serialization\Json;
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
/**
* Tests the UI preview functionality.
*
* @group views_ui
*/
class PreviewTest extends UITestBase {
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = array('test_preview', 'test_preview_error', 'test_pager_full', 'test_mini_pager', 'test_click_sort');
/**
* Tests contextual links in the preview form.
*/
public function testPreviewContextual() {
\Drupal::service('module_installer')->install(array('contextual'));
$this->resetAll();
$this->drupalGet('admin/structure/views/view/test_preview/edit');
$this->assertResponse(200);
$this->drupalPostForm(NULL, $edit = array(), t('Update preview'));
$elements = $this->xpath('//div[@id="views-live-preview"]//ul[contains(@class, :ul-class)]/li[contains(@class, :li-class)]', array(':ul-class' => 'contextual-links', ':li-class' => 'filter-add'));
$this->assertEqual(count($elements), 1, 'The contextual link to add a new field is shown.');
$this->drupalPostForm(NULL, $edit = array('view_args' => '100'), t('Update preview'));
// Test that area text and exposed filters are present and rendered.
$this->assertFieldByName('id', NULL, 'ID exposed filter field found.');
$this->assertText('Test header text', 'Rendered header text found');
$this->assertText('Test footer text', 'Rendered footer text found.');
$this->assertText('Test empty text', 'Rendered empty text found.');
}
/**
* Tests arguments in the preview form.
*/
function testPreviewUI() {
$this->drupalGet('admin/structure/views/view/test_preview/edit');
$this->assertResponse(200);
$this->drupalPostForm(NULL, $edit = array(), t('Update preview'));
$elements = $this->xpath('//div[@class = "view-content"]/div[contains(@class, views-row)]');
$this->assertEqual(count($elements), 5);
// Filter just the first result.
$this->drupalPostForm(NULL, $edit = array('view_args' => '1'), t('Update preview'));
$elements = $this->xpath('//div[@class = "view-content"]/div[contains(@class, views-row)]');
$this->assertEqual(count($elements), 1);
// Filter for no results.
$this->drupalPostForm(NULL, $edit = array('view_args' => '100'), t('Update preview'));
$elements = $this->xpath('//div[@class = "view-content"]/div[contains(@class, views-row)]');
$this->assertEqual(count($elements), 0);
// Test that area text and exposed filters are present and rendered.
$this->assertFieldByName('id', NULL, 'ID exposed filter field found.');
$this->assertText('Test header text', 'Rendered header text found');
$this->assertText('Test footer text', 'Rendered footer text found.');
$this->assertText('Test empty text', 'Rendered empty text found.');
// Test feed preview.
$view = array();
$view['label'] = $this->randomMachineName(16);
$view['id'] = strtolower($this->randomMachineName(16));
$view['page[create]'] = 1;
$view['page[title]'] = $this->randomMachineName(16);
$view['page[path]'] = $this->randomMachineName(16);
$view['page[feed]'] = 1;
$view['page[feed_properties][path]'] = $this->randomMachineName(16);
$this->drupalPostForm('admin/structure/views/add', $view, t('Save and edit'));
$this->clickLink(t('Feed'));
$this->drupalPostForm(NULL, array(), t('Update preview'));
$result = $this->xpath('//div[@id="views-live-preview"]/pre');
$this->assertTrue(strpos($result[0], '<title>' . $view['page[title]'] . '</title>'), 'The Feed RSS preview was rendered.');
// Test the non-default UI display options.
// Statistics only, no query.
$settings = \Drupal::configFactory()->getEditable('views.settings');
$settings->set('ui.show.performance_statistics', TRUE)->save();
$this->drupalGet('admin/structure/views/view/test_preview/edit');
$this->drupalPostForm(NULL, $edit = array('view_args' => '100'), t('Update preview'));
$this->assertText(t('Query build time'));
$this->assertText(t('Query execute time'));
$this->assertText(t('View render time'));
$this->assertNoRaw('<strong>Query</strong>');
// Statistics and query.
$settings->set('ui.show.sql_query.enabled', TRUE)->save();
$this->drupalPostForm(NULL, $edit = array('view_args' => '100'), t('Update preview'));
$this->assertText(t('Query build time'));
$this->assertText(t('Query execute time'));
$this->assertText(t('View render time'));
$this->assertRaw('<strong>Query</strong>');
$this->assertText("SELECT views_test_data.name AS views_test_data_name\nFROM \n{views_test_data} views_test_data\nWHERE (( (views_test_data.id = &#039;100&#039; ) ))");
// Test that the statistics and query are rendered above the preview.
$this->assertTrue(strpos($this->getRawContent(), 'views-query-info') < strpos($this->getRawContent(), 'view-test-preview'), 'Statistics shown above the preview.');
// Test that statistics and query rendered below the preview.
$settings->set('ui.show.sql_query.where', 'below')->save();
$this->drupalPostForm(NULL, $edit = array('view_args' => '100'), t('Update preview'));
$this->assertTrue(strpos($this->getRawContent(), 'view-test-preview') < strpos($this->getRawContent(), 'views-query-info'), 'Statistics shown below the preview.');
}
/**
* Tests the taxonomy term preview AJAX.
*
* This tests a specific regression in the taxonomy term view preview.
*
* @see https://www.drupal.org/node/2452659
*/
public function testTaxonomyAJAX() {
\Drupal::service('module_installer')->install(array('taxonomy'));
$this->getPreviewAJAX('taxonomy_term', 'page_1', 0);
}
/**
* Tests pagers in the preview form.
*/
public function testPreviewWithPagersUI() {
// Create 11 nodes and make sure that everyone is returned.
$this->drupalCreateContentType(array('type' => 'page'));
for ($i = 0; $i < 11; $i++) {
$this->drupalCreateNode();
}
// Test Full Pager.
$this->getPreviewAJAX('test_pager_full', 'default', 5);
// Test that the pager is present and rendered.
$elements = $this->xpath('//ul[contains(@class, :class)]/li', array(':class' => 'pager__items'));
$this->assertTrue(!empty($elements), 'Full pager found.');
// Verify elements and links to pages.
// We expect to find 5 elements: current page == 1, links to pages 2 and
// and 3, links to 'next >' and 'last >>' pages.
$this->assertClass($elements[0], 'is-active', 'Element for current page has .is-active class.');
$this->assertTrue($elements[0]->a, 'Element for current page has link.');
$this->assertClass($elements[1], 'pager__item', 'Element for page 2 has .pager__item class.');
$this->assertTrue($elements[1]->a, 'Link to page 2 found.');
$this->assertClass($elements[2], 'pager__item', 'Element for page 3 has .pager__item class.');
$this->assertTrue($elements[2]->a, 'Link to page 3 found.');
$this->assertClass($elements[3], 'pager__item--next', 'Element for next page has .pager__item--next class.');
$this->assertTrue($elements[3]->a, 'Link to next page found.');
$this->assertClass($elements[4], 'pager__item--last', 'Element for last page has .pager__item--last class.');
$this->assertTrue($elements[4]->a, 'Link to last page found.');
// Navigate to next page.
$elements = $this->xpath('//li[contains(@class, :class)]/a', array(':class' => 'pager__item--next'));
$this->clickPreviewLinkAJAX($elements[0]['href'], 5);
// Test that the pager is present and rendered.
$elements = $this->xpath('//ul[contains(@class, :class)]/li', array(':class' => 'pager__items'));
$this->assertTrue(!empty($elements), 'Full pager found.');
// Verify elements and links to pages.
// We expect to find 7 elements: links to '<< first' and '< previous'
// pages, link to page 1, current page == 2, link to page 3 and links
// to 'next >' and 'last >>' pages.
$this->assertClass($elements[0], 'pager__item--first', 'Element for first page has .pager__item--first class.');
$this->assertTrue($elements[0]->a, 'Link to first page found.');
$this->assertClass($elements[1], 'pager__item--previous', 'Element for previous page has .pager__item--previous class.');
$this->assertTrue($elements[1]->a, 'Link to previous page found.');
$this->assertClass($elements[2], 'pager__item', 'Element for page 1 has .pager__item class.');
$this->assertTrue($elements[2]->a, 'Link to page 1 found.');
$this->assertClass($elements[3], 'is-active', 'Element for current page has .is-active class.');
$this->assertTrue($elements[3]->a, 'Element for current page has link.');
$this->assertClass($elements[4], 'pager__item', 'Element for page 3 has .pager__item class.');
$this->assertTrue($elements[4]->a, 'Link to page 3 found.');
$this->assertClass($elements[5], 'pager__item--next', 'Element for next page has .pager__item--next class.');
$this->assertTrue($elements[5]->a, 'Link to next page found.');
$this->assertClass($elements[6], 'pager__item--last', 'Element for last page has .pager__item--last class.');
$this->assertTrue($elements[6]->a, 'Link to last page found.');
// Test Mini Pager.
$this->getPreviewAJAX('test_mini_pager', 'default', 3);
// Test that the pager is present and rendered.
$elements = $this->xpath('//ul[contains(@class, :class)]/li', array(':class' => 'pager__items'));
$this->assertTrue(!empty($elements), 'Mini pager found.');
// Verify elements and links to pages.
// We expect to find current pages element with no link, next page element
// with a link, and not to find previous page element.
$this->assertClass($elements[0], 'is-active', 'Element for current page has .is-active class.');
$this->assertClass($elements[1], 'pager__item--next', 'Element for next page has .pager__item--next class.');
$this->assertTrue($elements[1]->a, 'Link to next page found.');
// Navigate to next page.
$elements = $this->xpath('//li[contains(@class, :class)]/a', array(':class' => 'pager__item--next'));
$this->clickPreviewLinkAJAX($elements[0]['href'], 3);
// Test that the pager is present and rendered.
$elements = $this->xpath('//ul[contains(@class, :class)]/li', array(':class' => 'pager__items'));
$this->assertTrue(!empty($elements), 'Mini pager found.');
// Verify elements and links to pages.
// We expect to find 3 elements: previous page with a link, current
// page with no link, and next page with a link.
$this->assertClass($elements[0], 'pager__item--previous', 'Element for previous page has .pager__item--previous class.');
$this->assertTrue($elements[0]->a, 'Link to previous page found.');
$this->assertClass($elements[1], 'is-active', 'Element for current page has .is-active class.');
$this->assertFalse(isset($elements[1]->a), 'Element for current page has no link.');
$this->assertClass($elements[2], 'pager__item--next', 'Element for next page has .pager__item--next class.');
$this->assertTrue($elements[2]->a, 'Link to next page found.');
}
/**
* Tests the additional information query info area.
*/
public function testPreviewAdditionalInfo() {
\Drupal::service('module_installer')->install(array('views_ui_test'));
$this->resetAll();
$this->drupalGet('admin/structure/views/view/test_preview/edit');
$this->assertResponse(200);
$this->drupalPostForm(NULL, $edit = array(), t('Update preview'));
// Check for implementation of hook_views_preview_info_alter().
// @see views_ui_test.module
$elements = $this->xpath('//div[@id="views-live-preview"]/div[contains(@class, views-query-info)]//td[text()=:text]', array(':text' => t('Test row count')));
$this->assertEqual(count($elements), 1, 'Views Query Preview Info area altered.');
// Check that additional assets are attached.
$this->assertTrue(strpos($this->getDrupalSettings()['ajaxPageState']['libraries'], 'views_ui_test/views_ui_test.test') !== FALSE, 'Attached library found.');
$this->assertRaw('css/views_ui_test.test.css', 'Attached CSS asset found.');
}
/**
* Tests view validation error messages in the preview.
*/
public function testPreviewError() {
$this->drupalGet('admin/structure/views/view/test_preview_error/edit');
$this->assertResponse(200);
$this->drupalPostForm(NULL, $edit = array(), t('Update preview'));
$this->assertText('Unable to preview due to validation errors.', 'Preview error text found.');
}
/**
* Tests the link to sort in the preview form.
*/
public function testPreviewSortLink() {
// Get the preview.
$this->getPreviewAJAX('test_click_sort', 'page_1', 0);
// Test that the header label is present.
$elements = $this->xpath('//th[contains(@class, :class)]/a', array(':class' => 'views-field views-field-name'));
$this->assertTrue(!empty($elements), 'The header label is present.');
// Verify link.
$this->assertLinkByHref('preview/page_1?_wrapper_format=drupal_ajax&order=name&sort=desc', 0, 'The output URL is as expected.');
// Click link to sort.
$this->clickPreviewLinkAJAX($elements[0]['href'], 0);
// Test that the header label is present.
$elements = $this->xpath('//th[contains(@class, :class)]/a', array(':class' => 'views-field views-field-name is-active'));
$this->assertTrue(!empty($elements), 'The header label is present.');
// Verify link.
$this->assertLinkByHref('preview/page_1?_wrapper_format=drupal_ajax&order=name&sort=asc', 0, 'The output URL is as expected.');
}
/**
* Get the preview form and force an AJAX preview update.
*
* @param string $view_name
* The view to test.
* @param string $panel_id
* The view panel to test.
* @param int $row_count
* The expected number of rows in the preview.
*/
protected function getPreviewAJAX($view_name, $panel_id, $row_count) {
$this->drupalGet('admin/structure/views/view/' . $view_name . '/preview/' . $panel_id);
$result = $this->drupalPostAjaxForm(NULL, array(), array('op' => t('Update preview')));
$this->assertPreviewAJAX($result, $row_count);
}
/**
* Mimic clicking on a preview link.
*
* @param string $url
* The url to navigate to.
* @param int $row_count
* The expected number of rows in the preview.
*/
protected function clickPreviewLinkAJAX($url, $row_count) {
$content = $this->content;
$drupal_settings = $this->drupalSettings;
$ajax_settings = array(
'wrapper' => 'views-preview-wrapper',
'method' => 'replaceWith',
);
$url = $this->getAbsoluteUrl($url);
$post = array('js' => 'true') + $this->getAjaxPageStatePostData();
$result = Json::decode($this->drupalPost($url, '', $post, ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']]));
if (!empty($result)) {
$this->drupalProcessAjaxResponse($content, $result, $ajax_settings, $drupal_settings);
}
$this->assertPreviewAJAX($result, $row_count);
}
/**
* Assert that the AJAX response contains expected data.
*
* @param array $result
* An array of AJAX commands.
* @param int $row_count
* The expected number of rows in the preview.
*/
protected function assertPreviewAJAX($result, $row_count) {
// Has AJAX callback replied with an insert command? If so, we can
// assume that the page content was updated with AJAX returned data.
$result_commands = array();
foreach ($result as $command) {
$result_commands[$command['command']] = $command;
}
$this->assertTrue(isset($result_commands['insert']), 'AJAX insert command received.');
// Test if preview contains the expected number of rows.
$elements = $this->xpath('//div[@class = "view-content"]/div[contains(@class, views-row)]');
$this->assertEqual(count($elements), $row_count, 'Expected items found on page.');
}
/**
* Asserts that an element has a given class.
*
* @param \SimpleXMLElement $element
* The element to test.
* @param string $class
* The class to assert.
* @param string $message
* (optional) A verbose message to output.
*/
protected function assertClass(\SimpleXMLElement $element, $class, $message = NULL) {
if (!isset($message)) {
$message = "Class .$class found.";
}
$this->assertTrue(strpos($element['class'], $class) !== FALSE, $message);
}
}

View file

@ -0,0 +1,54 @@
<?php
namespace Drupal\views_ui\Tests;
use Drupal\views\Views;
use Drupal\views\Entity\View;
/**
* Tests query plugins.
*
* @group views_ui
*/
class QueryTest extends UITestBase {
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = array('test_view');
/**
* {@inheritdoc}
*/
protected function viewsData() {
$data = parent::viewsData();
$data['views_test_data']['table']['base']['query_id'] = 'query_test';
return $data;
}
/**
* Tests query plugins settings.
*/
public function testQueryUI() {
$view = View::load('test_view');
$display = &$view->getDisplay('default');
$display['display_options']['query'] = ['type' => 'query_test'];
$view->save();
// Save some query settings.
$query_settings_path = "admin/structure/views/nojs/display/test_view/default/query";
$random_value = $this->randomMachineName();
$this->drupalPostForm($query_settings_path, array('query[options][test_setting]' => $random_value), t('Apply'));
$this->drupalPostForm(NULL, array(), t('Save'));
// Check that the settings are saved into the view itself.
$view = Views::getView('test_view');
$view->initDisplay();
$view->initQuery();
$this->assertEqual($random_value, $view->query->options['test_setting'], 'Query settings got saved');
}
}

View file

@ -0,0 +1,78 @@
<?php
namespace Drupal\views_ui\Tests;
use Drupal\views\Views;
/**
* Tests the reordering of fields via AJAX.
*
* @group views_ui
* @see \Drupal\views_ui\Form\Ajax\Rearrange
*/
class RearrangeFieldsTest extends UITestBase {
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = array('test_view');
/**
* Gets the fields from the View.
*/
protected function getViewFields($view_name = 'test_view', $display_id = 'default') {
$view = Views::getView($view_name);
$view->setDisplay($display_id);
$fields = array();
foreach ($view->displayHandlers->get('default')->getHandlers('field') as $field => $handler) {
$fields[] = $field;
}
return $fields;
}
/**
* Check if the fields are in the correct order.
*
* @param $view_name
* The name of the view.
* @param $fields
* Array of field names.
*/
protected function assertFieldOrder($view_name, $fields) {
$this->drupalGet('admin/structure/views/nojs/rearrange/' . $view_name . '/default/field');
foreach ($fields as $idx => $field) {
$this->assertFieldById('edit-fields-' . $field . '-weight', $idx + 1);
}
}
/**
* Tests field sorting.
*/
public function testRearrangeFields() {
$view_name = 'test_view';
// Checks that the order on the rearrange form matches the creation order.
$this->assertFieldOrder($view_name, $this->getViewFields($view_name));
// Checks that a field is not deleted if a value is not passed back.
$fields = array();
$this->drupalPostForm('admin/structure/views/nojs/rearrange/' . $view_name . '/default/field', $fields, t('Apply'));
$this->assertFieldOrder($view_name, $this->getViewFields($view_name));
// Checks that revers the new field order is respected.
$reversedFields = array_reverse($this->getViewFields($view_name));
$fields = array();
foreach ($reversedFields as $delta => $field) {
$fields['fields[' . $field . '][weight]'] = $delta;
}
$this->drupalPostForm('admin/structure/views/nojs/rearrange/' . $view_name . '/default/field', $fields, t('Apply'));
$this->assertFieldOrder($view_name, $reversedFields);
// Checks that there is a remove link for each field.
$this->assertEqual(count($this->cssSelect('a.views-remove-link')), count($fields));
}
}

View file

@ -0,0 +1,45 @@
<?php
namespace Drupal\views_ui\Tests;
/**
* Tests the redirecting after saving a views.
*
* @group views_ui
*/
class RedirectTest extends UITestBase {
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = array('test_view', 'test_redirect_view');
/**
* Tests the redirecting.
*/
public function testRedirect() {
$view_name = 'test_view';
$random_destination = $this->randomMachineName();
$edit_path = "admin/structure/views/view/$view_name/edit";
$this->drupalPostForm($edit_path, array(), t('Save'), array('query' => array('destination' => $random_destination)));
$this->assertUrl($random_destination, array(), 'Make sure the user got redirected to the expected page defined in the destination.');
// Setup a view with a certain page display path. If you change the path
// but have the old url in the destination the user should be redirected to
// the new path.
$view_name = 'test_redirect_view';
$new_path = $this->randomMachineName();
$edit_path = "admin/structure/views/view/$view_name/edit";
$path_edit_path = "admin/structure/views/nojs/display/$view_name/page_1/path";
$this->drupalPostForm($path_edit_path, array('path' => $new_path), t('Apply'));
$this->drupalPostForm($edit_path, array(), t('Save'), array('query' => array('destination' => 'test-redirect-view')));
$this->assertUrl($new_path, array(), 'Make sure the user got redirected to the expected page after changing the URL of a page display.');
}
}

View file

@ -0,0 +1,55 @@
<?php
namespace Drupal\views_ui\Tests;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
/**
* Tests the Views fields report page.
*
* @group views_ui
*/
class ReportFieldsTest extends UITestBase {
/**
* {@inheritdoc}
*/
public static $testViews = ['test_field_field_test'];
/**
* {@inheritdoc}
*/
public static $modules = ['entity_test'];
/**
* Tests the Views fields report page.
*/
public function testReportFields() {
$this->drupalGet('admin/reports/fields/views-fields');
$this->assertRaw('Used in views', 'Title appears correctly');
$this->assertRaw('No fields have been used in views yet.', 'No results message appears correctly.');
// Set up the field_test field.
$field_storage = FieldStorageConfig::create([
'field_name' => 'field_test',
'type' => 'integer',
'entity_type' => 'entity_test',
]);
$field_storage->save();
$field = FieldConfig::create([
'field_name' => 'field_test',
'entity_type' => 'entity_test',
'bundle' => 'entity_test',
]);
$field->save();
$this->drupalGet('admin/reports/fields/views-fields');
// Assert that the newly created field appears in the overview.
$this->assertRaw('<td>field_test</td>', 'Field name appears correctly');
$this->assertRaw('>test_field_field_test</a>', 'View name appears correctly');
$this->assertRaw('Used in views', 'Title appears correctly');
}
}

View file

@ -0,0 +1,43 @@
<?php
namespace Drupal\views_ui\Tests;
use Drupal\simpletest\WebTestBase;
/**
* Tests existence of the views plugin report.
*
* @group views_ui
*/
class ReportTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('views', 'views_ui');
/**
* Stores an admin user used by the different tests.
*
* @var \Drupal\user\User
*/
protected $adminUser;
protected function setUp() {
parent::setUp();
$this->adminUser = $this->drupalCreateUser(array('administer views'));
}
/**
* Tests the existence of the views plugin report.
*/
public function testReport() {
$this->drupalLogin($this->adminUser);
// Test the report page.
$this->drupalGet('admin/reports/views-plugins');
$this->assertResponse(200, "Views report page exists");
}
}

View file

@ -0,0 +1,95 @@
<?php
namespace Drupal\views_ui\Tests;
use Drupal\Core\Entity\Entity\EntityViewMode;
use Drupal\views\Views;
/**
* Tests the UI of row plugins.
*
* @group views_ui
* @see \Drupal\views_test_data\Plugin\views\row\RowTest.
*/
class RowUITest extends UITestBase {
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = array('test_view');
/**
* Tests changing the row plugin and changing some options of a row.
*/
public function testRowUI() {
$view_name = 'test_view';
$view_edit_url = "admin/structure/views/view/$view_name/edit";
$row_plugin_url = "admin/structure/views/nojs/display/$view_name/default/row";
$row_options_url = "admin/structure/views/nojs/display/$view_name/default/row_options";
$this->drupalGet($row_plugin_url);
$this->assertFieldByName('row[type]', 'fields', 'The default row plugin selected in the UI should be fields.');
$edit = array(
'row[type]' => 'test_row'
);
$this->drupalPostForm(NULL, $edit, t('Apply'));
$this->assertFieldByName('row_options[test_option]', NULL, 'Make sure the custom settings form from the test plugin appears.');
$random_name = $this->randomMachineName();
$edit = array(
'row_options[test_option]' => $random_name
);
$this->drupalPostForm(NULL, $edit, t('Apply'));
$this->drupalGet($row_options_url);
$this->assertFieldByName('row_options[test_option]', $random_name, 'Make sure the custom settings form field has the expected value stored.');
$this->drupalPostForm($view_edit_url, array(), t('Save'));
$this->assertLink(t('Test row plugin'), 0, 'Make sure the test row plugin is shown in the UI');
$view = Views::getView($view_name);
$view->initDisplay();
$row = $view->display_handler->getOption('row');
$this->assertEqual($row['type'], 'test_row', 'Make sure that the test_row got saved as used row plugin.');
$this->assertEqual($row['options']['test_option'], $random_name, 'Make sure that the custom settings field got saved as expected.');
// Change the row plugin to fields using ajax.
// Note: this is the best approximation we can achieve, because we cannot
// simulate the 'openDialog' command in
// WebTestBase::drupalProcessAjaxResponse(), hence we have to make do.
$row_plugin_url_ajax = str_replace('/nojs/', '/ajax/', $row_plugin_url);
$ajax_settings = [
'accepts' => 'application/vnd.drupal-ajax',
'submit' => [
'_triggering_element_name' => 'op',
'_triggering_element_value' => 'Apply',
],
'url' => $row_plugin_url_ajax,
];
$this->drupalPostAjaxForm($row_plugin_url, ['row[type]' => 'fields'], NULL, $row_plugin_url_ajax, [], [], NULL, $ajax_settings);
$this->drupalGet($row_plugin_url);
$this->assertResponse(200);
$this->assertFieldByName('row[type]', 'fields', 'Make sure that the fields got saved as used row plugin.');
// Ensure that entity row plugins appear.
$view_name = 'content';
$row_plugin_url = "admin/structure/views/nojs/display/$view_name/default/row";
$row_options_url = "admin/structure/views/nojs/display/$view_name/default/row_options";
$this->drupalGet($row_plugin_url);
$this->assertFieldByName('row[type]', 'entity:node');
$this->drupalPostForm(NULL, ['row[type]' => 'entity:node'], t('Apply'));
$this->assertUrl($row_options_url);
$this->assertFieldByName('row_options[view_mode]', 'teaser');
// Change the teaser label to have markup so we can test escaping.
$teaser = EntityViewMode::load('node.teaser');
$teaser->set('label', 'Teaser <em>markup</em>');
$teaser->save();
$this->drupalGet('admin/structure/views/view/frontpage/edit/default');
$this->assertEscaped('Teaser <em>markup</em>');
}
}

View file

@ -0,0 +1,142 @@
<?php
namespace Drupal\views_ui\Tests;
/**
* Tests all ui related settings under admin/structure/views/settings.
*
* @group views_ui
*/
class SettingsTest extends UITestBase {
/**
* Stores an admin user used by the different tests.
*
* @var \Drupal\user\User
*/
protected $adminUser;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->drupalPlaceBlock('local_tasks_block');
}
/**
* Tests the settings for the edit ui.
*/
function testEditUI() {
$this->drupalLogin($this->adminUser);
// Test the settings tab exists.
$this->drupalGet('admin/structure/views');
$this->assertLinkByHref('admin/structure/views/settings');
// Test the confirmation message.
$this->drupalPostForm('admin/structure/views/settings', array(), t('Save configuration'));
$this->assertText(t('The configuration options have been saved.'));
// Configure to always show the master display.
$edit = array(
'ui_show_master_display' => TRUE,
);
$this->drupalPostForm('admin/structure/views/settings', $edit, t('Save configuration'));
$view = array();
$view['label'] = $this->randomMachineName(16);
$view['id'] = strtolower($this->randomMachineName(16));
$view['description'] = $this->randomMachineName(16);
$view['page[create]'] = TRUE;
$view['page[title]'] = $this->randomMachineName(16);
$view['page[path]'] = $this->randomMachineName(16);
$this->drupalPostForm('admin/structure/views/add', $view, t('Save and edit'));
// Configure to not always show the master display.
// If you have a view without a page or block the master display should be
// still shown.
$edit = array(
'ui_show_master_display' => FALSE,
);
$this->drupalPostForm('admin/structure/views/settings', $edit, t('Save configuration'));
$view['page[create]'] = FALSE;
$this->drupalPostForm('admin/structure/views/add', $view, t('Save and edit'));
// Create a view with an additional display, so master should be hidden.
$view['page[create]'] = TRUE;
$view['id'] = strtolower($this->randomMachineName());
$this->drupalPostForm('admin/structure/views/add', $view, t('Save and edit'));
$this->assertNoLink(t('Master'));
// Configure to always show the advanced settings.
// @todo It doesn't seem to be a way to test this as this works just on js.
// Configure to show the embeddable display.
$edit = array(
'ui_show_display_embed' => TRUE,
);
$this->drupalPostForm('admin/structure/views/settings', $edit, t('Save configuration'));
$view['id'] = strtolower($this->randomMachineName());
$this->drupalPostForm('admin/structure/views/add', $view, t('Save and edit'));
$this->assertFieldById('edit-displays-top-add-display-embed');
$edit = array(
'ui_show_display_embed' => FALSE,
);
$this->drupalPostForm('admin/structure/views/settings', $edit, t('Save configuration'));
$this->drupalPostForm('admin/structure/views/add', $view, t('Save and edit'));
$this->assertNoFieldById('edit-displays-top-add-display-embed');
// Configure to hide/show the sql at the preview.
$edit = array(
'ui_show_sql_query_enabled' => FALSE,
);
$this->drupalPostForm('admin/structure/views/settings', $edit, t('Save configuration'));
$view['id'] = strtolower($this->randomMachineName());
$this->drupalPostForm('admin/structure/views/add', $view, t('Save and edit'));
$this->drupalPostForm(NULL, array(), t('Update preview'));
$xpath = $this->xpath('//div[@class="views-query-info"]/pre');
$this->assertEqual(count($xpath), 0, 'The views sql is hidden.');
$edit = array(
'ui_show_sql_query_enabled' => TRUE,
);
$this->drupalPostForm('admin/structure/views/settings', $edit, t('Save configuration'));
$view['id'] = strtolower($this->randomMachineName());
$this->drupalPostForm('admin/structure/views/add', $view, t('Save and edit'));
$this->drupalPostForm(NULL, array(), t('Update preview'));
$xpath = $this->xpath('//div[@class="views-query-info"]//pre');
$this->assertEqual(count($xpath), 1, 'The views sql is shown.');
$this->assertFalse(strpos($xpath[0], 'db_condition_placeholder') !== FALSE, 'No placeholders are shown in the views sql.');
$this->assertTrue(strpos($xpath[0], "node_field_data.status = '1'") !== FALSE, 'The placeholders in the views sql is replace by the actual value.');
// Test the advanced settings form.
// Test the confirmation message.
$this->drupalPostForm('admin/structure/views/settings/advanced', array(), t('Save configuration'));
$this->assertText(t('The configuration options have been saved.'));
$edit = array(
'skip_cache' => TRUE,
'sql_signature' => TRUE,
);
$this->drupalPostForm('admin/structure/views/settings/advanced', $edit, t('Save configuration'));
$this->assertFieldChecked('edit-skip-cache', 'The skip_cache option is checked.');
$this->assertFieldChecked('edit-sql-signature', 'The sql_signature option is checked.');
// Test the "Clear Views' cache" button.
$this->drupalPostForm('admin/structure/views/settings/advanced', array(), t("Clear Views' cache"));
$this->assertText(t('The cache has been cleared.'));
}
}

View file

@ -0,0 +1,56 @@
<?php
namespace Drupal\views_ui\Tests;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\views\Views;
/**
* Tests the UI of storage properties of views.
*
* @group views_ui
*/
class StorageTest extends UITestBase {
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = array('test_view');
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('views_ui', 'language');
/**
* Tests changing label, description and tag.
*
* @see views_ui_edit_details_form
*/
public function testDetails() {
$view_name = 'test_view';
ConfigurableLanguage::createFromLangcode('fr')->save();
$edit = array(
'label' => $this->randomMachineName(),
'tag' => $this->randomMachineName(),
'description' => $this->randomMachineName(30),
'langcode' => 'fr',
);
$this->drupalPostForm("admin/structure/views/nojs/edit-details/$view_name/default", $edit, t('Apply'));
$this->drupalPostForm(NULL, array(), t('Save'));
$view = Views::getView($view_name);
foreach (array('label', 'tag', 'description', 'langcode') as $property) {
$this->assertEqual($view->storage->get($property), $edit[$property], format_string('Make sure the property @property got probably saved.', array('@property' => $property)));
}
}
}

View file

@ -0,0 +1,35 @@
<?php
namespace Drupal\views_ui\Tests;
use Drupal\views\Views;
/**
* Tests the UI of views when using the table style.
*
* @group views_ui
* @see \Drupal\views\Plugin\views\style\Table.
*/
class StyleTableTest extends UITestBase {
/**
* Tests created a table style view.
*/
public function testWizard() {
// Create a new view and check that the first field has a label.
$view = array();
$view['label'] = $this->randomMachineName(16);
$view['id'] = strtolower($this->randomMachineName(16));
$view['show[wizard_key]'] = 'node';
$view['page[create]'] = TRUE;
$view['page[style][style_plugin]'] = 'table';
$view['page[title]'] = $this->randomMachineName(16);
$view['page[path]'] = $view['id'];
$this->drupalPostForm('admin/structure/views/add', $view, t('Save and edit'));
$view = Views::getView($view['id']);
$view->initHandlers();
$this->assertEqual($view->field['title']->options['label'], 'Title', 'The field label for table styles is not empty.');
}
}

View file

@ -0,0 +1,65 @@
<?php
namespace Drupal\views_ui\Tests;
use Drupal\views\Views;
/**
* Tests the UI of style plugins.
*
* @group views_ui
* @see \Drupal\views_test_data\Plugin\views\style\StyleTest.
*/
class StyleUITest extends UITestBase {
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = array('test_view');
/**
* Tests changing the style plugin and changing some options of a style.
*/
public function testStyleUI() {
$view_name = 'test_view';
$view_edit_url = "admin/structure/views/view/$view_name/edit";
$style_plugin_url = "admin/structure/views/nojs/display/$view_name/default/style";
$style_options_url = "admin/structure/views/nojs/display/$view_name/default/style_options";
$this->drupalGet($style_plugin_url);
$this->assertFieldByName('style[type]', 'default', 'The default style plugin selected in the UI should be unformatted list.');
$edit = array(
'style[type]' => 'test_style'
);
$this->drupalPostForm(NULL, $edit, t('Apply'));
$this->assertFieldByName('style_options[test_option]', NULL, 'Make sure the custom settings form from the test plugin appears.');
$random_name = $this->randomMachineName();
$edit = array(
'style_options[test_option]' => $random_name
);
$this->drupalPostForm(NULL, $edit, t('Apply'));
$this->drupalGet($style_options_url);
$this->assertFieldByName('style_options[test_option]', $random_name, 'Make sure the custom settings form field has the expected value stored.');
$this->drupalPostForm($view_edit_url, array(), t('Save'));
$this->assertLink(t('Test style plugin'), 0, 'Make sure the test style plugin is shown in the UI');
$view = Views::getView($view_name);
$view->initDisplay();
$style = $view->display_handler->getOption('style');
$this->assertEqual($style['type'], 'test_style', 'Make sure that the test_style got saved as used style plugin.');
$this->assertEqual($style['options']['test_option'], $random_name, 'Make sure that the custom settings field got saved as expected.');
// Test that fields are working correctly in the UI for style plugins when
// a field row plugin is selected.
$this->drupalPostForm("admin/structure/views/view/$view_name/edit", array(), 'Add Page');
$this->drupalPostForm("admin/structure/views/nojs/display/$view_name/page_1/row", array('row[type]' => 'fields'), t('Apply'));
// If fields are being used this text will not be shown.
$this->assertNoText(t('The selected style or row format does not use fields.'));
}
}

View file

@ -0,0 +1,45 @@
<?php
namespace Drupal\views_ui\Tests;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\views\Entity\View;
/**
* Tests the token display for the TokenizeAreaPluginBase UI.
*
* @see \Drupal\views\Plugin\views\area\Entity
* @group views_ui
*/
class TokenizeAreaUITest extends UITestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['entity_test'];
/**
* Test that the right tokens are shown as available for replacement.
*/
public function testTokenUI() {
$entity_test = EntityTest::create(['bundle' => 'entity_test']);
$entity_test->save();
$default = $this->randomView([]);
$id = $default['id'];
$view = View::load($id);
$this->drupalGet($view->toUrl('edit-form'));
// Add a global NULL argument to the view for testing argument tokens.
$this->drupalPostForm("admin/structure/views/nojs/add-handler/$id/page_1/argument", ['name[views.null]' => 1], 'Add and configure contextual filters');
$this->drupalPostForm(NULL, [], 'Apply');
$this->drupalPostForm("admin/structure/views/nojs/add-handler/$id/page_1/header", ['name[views.area]' => 'views.area'], 'Add and configure header');
// Test that field tokens are shown.
$this->assertText('{{ title }} == Content: Title');
// Test that argument tokens are shown.
$this->assertText('{{ arguments.null }} == Global: Null title');
}
}

View file

@ -0,0 +1,84 @@
<?php
namespace Drupal\views_ui\Tests;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\simpletest\WebTestBase;
/**
* Tests that translated strings in views UI don't override original strings.
*
* @group views_ui
*/
class TranslatedViewTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = [
'config_translation',
'views_ui',
];
/**
* Languages to enable.
*
* @var array
*/
protected $langcodes = [
'fr',
];
/**
* Administrator user for tests.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
protected function setUp() {
parent::setUp();
$permissions = [
'administer site configuration',
'administer views',
'translate configuration',
'translate interface',
];
// Create and log in user.
$this->adminUser = $this->drupalCreateUser($permissions);
$this->drupalLogin($this->adminUser);
// Add languages.
foreach ($this->langcodes as $langcode) {
ConfigurableLanguage::createFromLangcode($langcode)->save();
}
$this->resetAll();
$this->rebuildContainer();
}
public function testTranslatedStrings() {
$translation_url = 'admin/structure/views/view/files/translate/fr/add';
$edit_url = 'admin/structure/views/view/files';
// Check origial string.
$this->drupalGet($edit_url);
$this->assertTitle('Files (File) | Drupal');
// Translate the label of the view.
$this->drupalGet($translation_url);
$edit = [
'translation[config_names][views.view.files][label]' => 'Fichiers',
];
$this->drupalPostForm(NULL, $edit, t('Save translation'));
// Check if the label is translated.
$this->drupalGet($edit_url, ['language' => \Drupal::languageManager()->getLanguage('fr')]);
$this->assertTitle('Files (File) | Drupal');
$this->assertNoText('Fichiers');
}
}

View file

@ -0,0 +1,90 @@
<?php
namespace Drupal\views_ui\Tests;
use Drupal\views\Tests\ViewTestBase;
/**
* Provides a base class for testing the Views UI.
*/
abstract class UITestBase extends ViewTestBase {
/**
* An admin user with the 'administer views' permission.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* An admin user with administrative permissions for views, blocks, and nodes.
*
* @var \Drupal\user\UserInterface
*/
protected $fullAdminUser;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('node', 'views_ui', 'block', 'taxonomy');
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->enableViewsTestModule();
$this->adminUser = $this->drupalCreateUser(array('administer views'));
$this->fullAdminUser = $this->drupalCreateUser(array('administer views',
'administer blocks',
'bypass node access',
'access user profiles',
'view all revisions',
'administer permissions',
));
$this->drupalLogin($this->fullAdminUser);
}
/**
* A helper method which creates a random view.
*/
public function randomView(array $view = array()) {
// Create a new view in the UI.
$default = array();
$default['label'] = $this->randomMachineName(16);
$default['id'] = strtolower($this->randomMachineName(16));
$default['description'] = $this->randomMachineName(16);
$default['page[create]'] = TRUE;
$default['page[path]'] = $default['id'];
$view += $default;
$this->drupalPostForm('admin/structure/views/add', $view, t('Save and edit'));
return $default;
}
/**
* {@inheritdoc}
*/
protected function drupalGet($path, array $options = array(), array $headers = array()) {
$url = $this->buildUrl($path, $options);
// Ensure that each nojs page is accessible via ajax as well.
if (strpos($url, 'nojs') !== FALSE) {
$url = str_replace('nojs', 'ajax', $url);
$result = $this->drupalGet($url, $options, $headers);
$this->assertResponse(200);
$this->assertHeader('Content-Type', 'application/json');
$this->assertTrue(json_decode($result), 'Ensure that the AJAX request returned valid content.');
}
return parent::drupalGet($path, $options, $headers);
}
}

View file

@ -0,0 +1,79 @@
<?php
namespace Drupal\views_ui\Tests;
use Drupal\views\Tests\ViewTestBase;
/**
* Tests covering Preview of unsaved Views.
*
* @group views_ui
*/
class UnsavedPreviewTest extends ViewTestBase {
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = ['content'];
/**
* An admin user with the 'administer views' permission.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* {@inheritdoc}
*/
public static $modules = array('node', 'views_ui');
/**
* Sets up a Drupal site for running functional and integration tests.
*/
protected function setUp() {
parent::setUp(FALSE);
$this->adminUser = $this->drupalCreateUser(['administer views']);
$this->drupalLogin($this->adminUser);
}
/**
* Tests previews of unsaved new page displays.
*/
public function testUnsavedPageDisplayPreview() {
$this->drupalCreateContentType(['type' => 'page']);
for ($i = 0; $i < 5; $i++) {
$this->drupalCreateNode();
}
$this->drupalGet('admin/structure/views/view/content');
$this->assertResponse(200);
$this->drupalPostForm(NULL, [], t('Add Page'));
$this->assertResponse(200);
$this->drupalGet('admin/structure/views/nojs/display/content/page_2/path');
$this->assertResponse(200);
$this->drupalPostForm(NULL, ['path' => 'foobarbaz'], t('Apply'));
$this->assertResponse(200);
$this->drupalPostForm(NULL, [], t('Update preview'));
$this->assertResponse(200);
$this->assertText(t('This display has no path'));
$this->drupalGet('admin/structure/views/view/content/edit/page_2');
$this->assertResponse(200);
$this->drupalPostForm(NULL, [], t('Save'));
$this->assertResponse(200);
$this->drupalPostForm(NULL, [], t('Update preview'));
$this->assertResponse(200);
$this->assertLinkByHref('foobarbaz');
}
}

View file

@ -0,0 +1,236 @@
<?php
namespace Drupal\views_ui\Tests;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\views\Entity\View;
/**
* Tests some general functionality of editing views, like deleting a view.
*
* @group views_ui
*/
class ViewEditTest extends UITestBase {
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = array('test_view', 'test_display', 'test_groupwise_term_ui');
/**
* Tests the delete link on a views UI.
*/
public function testDeleteLink() {
$this->drupalGet('admin/structure/views/view/test_view');
$this->assertLink(t('Delete view'), 0, 'Ensure that the view delete link appears');
$view = $this->container->get('entity.manager')->getStorage('view')->load('test_view');
$this->assertTrue($view instanceof View);
$this->clickLink(t('Delete view'));
$this->assertUrl('admin/structure/views/view/test_view/delete');
$this->drupalPostForm(NULL, array(), t('Delete'));
$this->assertRaw(t('The view %name has been deleted.', array('%name' => $view->label())));
$this->assertUrl('admin/structure/views');
$view = $this->container->get('entity.manager')->getStorage('view')->load('test_view');
$this->assertFalse($view instanceof View);
}
/**
* Tests the machine name and administrative comment forms.
*/
public function testOtherOptions() {
$this->drupalGet('admin/structure/views/view/test_view');
// Add a new attachment display.
$this->drupalPostForm(NULL, array(), 'Add Attachment');
// Test that a long administrative comment is truncated.
$edit = array('display_comment' => 'one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen');
$this->drupalPostForm('admin/structure/views/nojs/display/test_view/attachment_1/display_comment', $edit, 'Apply');
$this->assertText('one two three four five six seven eight nine ten eleven twelve thirteen fourteen...');
// Change the machine name for the display from page_1 to test_1.
$edit = array('display_id' => 'test_1');
$this->drupalPostForm('admin/structure/views/nojs/display/test_view/attachment_1/display_id', $edit, 'Apply');
$this->assertLink(t('test_1'));
// Save the view, and test the new ID has been saved.
$this->drupalPostForm(NULL, array(), 'Save');
$view = \Drupal::entityManager()->getStorage('view')->load('test_view');
$displays = $view->get('display');
$this->assertTrue(!empty($displays['test_1']), 'Display data found for new display ID key.');
$this->assertIdentical($displays['test_1']['id'], 'test_1', 'New display ID matches the display ID key.');
$this->assertFalse(array_key_exists('attachment_1', $displays), 'Old display ID not found.');
// Test the form validation with invalid IDs.
$machine_name_edit_url = 'admin/structure/views/nojs/display/test_view/test_1/display_id';
$error_text = t('Display name must be letters, numbers, or underscores only.');
// Test that potential invalid display ID requests are detected
$this->drupalGet('admin/structure/views/ajax/handler/test_view/fake_display_name/filter/title');
$this->assertText('Invalid display id fake_display_name');
$edit = array('display_id' => 'test 1');
$this->drupalPostForm($machine_name_edit_url, $edit, 'Apply');
$this->assertText($error_text);
$edit = array('display_id' => 'test_1#');
$this->drupalPostForm($machine_name_edit_url, $edit, 'Apply');
$this->assertText($error_text);
// Test using an existing display ID.
$edit = array('display_id' => 'default');
$this->drupalPostForm($machine_name_edit_url, $edit, 'Apply');
$this->assertText(t('Display id should be unique.'));
// Test that the display ID has not been changed.
$this->drupalGet('admin/structure/views/view/test_view/edit/test_1');
$this->assertLink(t('test_1'));
// Test that validation does not run on cancel.
$this->drupalGet('admin/structure/views/view/test_view');
// Delete the field to cause an error on save.
$fields = [];
$fields['fields[age][removed]'] = 1;
$fields['fields[id][removed]'] = 1;
$fields['fields[name][removed]'] = 1;
$this->drupalPostForm('admin/structure/views/nojs/rearrange/test_view/default/field', $fields, t('Apply'));
$this->drupalPostForm(NULL, array(), 'Save');
$this->drupalPostForm(NULL, array(), t('Cancel'));
$this->assertNoFieldByXpath('//div[contains(@class, "error")]', FALSE, 'No error message is displayed.');
$this->assertUrl('admin/structure/views', array(), 'Redirected back to the view listing page..');
}
/**
* Tests the language options on the views edit form.
*/
public function testEditFormLanguageOptions() {
// Language options should not exist without language module.
$test_views = array(
'test_view' => 'default',
'test_display' => 'page_1',
);
foreach ($test_views as $view_name => $display) {
$this->drupalGet('admin/structure/views/view/' . $view_name);
$this->assertResponse(200);
$langcode_url = 'admin/structure/views/nojs/display/' . $view_name . '/' . $display . '/rendering_language';
$this->assertNoLinkByHref($langcode_url);
$this->assertNoLink(t('@type language selected for page', array('@type' => t('Content'))));
$this->assertNoLink(t('Content language of view row'));
}
// Make the site multilingual and test the options again.
$this->container->get('module_installer')->install(array('language', 'content_translation'));
ConfigurableLanguage::createFromLangcode('hu')->save();
$this->resetAll();
$this->rebuildContainer();
// Language options should now exist with entity language the default.
foreach ($test_views as $view_name => $display) {
$this->drupalGet('admin/structure/views/view/' . $view_name);
$this->assertResponse(200);
$langcode_url = 'admin/structure/views/nojs/display/' . $view_name . '/' . $display . '/rendering_language';
if ($view_name == 'test_view') {
$this->assertNoLinkByHref($langcode_url);
$this->assertNoLink(t('@type language selected for page', array('@type' => t('Content'))));
$this->assertNoLink(t('Content language of view row'));
}
else {
$this->assertLinkByHref($langcode_url);
$this->assertNoLink(t('@type language selected for page', array('@type' => t('Content'))));
$this->assertLink(t('Content language of view row'));
}
$this->drupalGet($langcode_url);
$this->assertResponse(200);
if ($view_name == 'test_view') {
$this->assertText(t('The view is not based on a translatable entity type or the site is not multilingual.'));
}
else {
$this->assertFieldByName('rendering_language', '***LANGUAGE_entity_translation***');
// Test that the order of the language list is similar to other language
// lists, such as in the content translation settings.
$expected_elements = array(
'***LANGUAGE_entity_translation***',
'***LANGUAGE_entity_default***',
'***LANGUAGE_site_default***',
'***LANGUAGE_language_interface***',
'en',
'hu',
);
$elements = $this->xpath('//select[@id="edit-rendering-language"]/option');
// Compare values inside the option elements with expected values.
for ($i = 0; $i < count($elements); $i++) {
$this->assertEqual($elements[$i]->attributes()->{'value'}, $expected_elements[$i]);
}
// Check that the selected values are respected even we they are not
// supposed to be listed.
// Give permission to edit languages to authenticated users.
$edit = [
'authenticated[administer languages]' => TRUE,
];
$this->drupalPostForm('/admin/people/permissions', $edit, t('Save permissions'));
// Enable Content language negotiation so we have one more item
// to select.
$edit = [
'language_content[configurable]' => TRUE,
];
$this->drupalPostForm('admin/config/regional/language/detection', $edit, t('Save settings'));
// Choose the new negotiation as the rendering language.
$edit = [
'rendering_language' => '***LANGUAGE_language_content***',
];
$this->drupalPostForm('/admin/structure/views/nojs/display/' . $view_name . '/' . $display . '/rendering_language', $edit, t('Apply'));
// Disable language content negotiation.
$edit = [
'language_content[configurable]' => FALSE,
];
$this->drupalPostForm('admin/config/regional/language/detection', $edit, t('Save settings'));
// Check that the previous selection is listed and selected.
$this->drupalGet($langcode_url);
$element = $this->xpath('//select[@id="edit-rendering-language"]/option[@value="***LANGUAGE_language_content***" and @selected="selected"]');
$this->assertFalse(empty($element), 'Current selection is not lost');
// Check the order for the langcode filter.
$langcode_url = 'admin/structure/views/nojs/handler/' . $view_name . '/' . $display . '/filter/langcode';
$this->drupalGet($langcode_url);
$this->assertResponse(200);
$expected_elements = array(
'all',
'***LANGUAGE_site_default***',
'***LANGUAGE_language_interface***',
'***LANGUAGE_language_content***',
'en',
'hu',
'und',
'zxx',
);
$elements = $this->xpath('//div[@id="edit-options-value"]//input');
// Compare values inside the option elements with expected values.
for ($i = 0; $i < count($elements); $i++) {
$this->assertEqual($elements[$i]->attributes()->{'value'}, $expected_elements[$i]);
}
}
}
}
/**
* Tests Representative Node for a Taxonomy Term.
*/
public function testRelationRepresentativeNode() {
// Populate and submit the form.
$edit["name[taxonomy_term_field_data.tid_representative]"] = TRUE;
$this->drupalPostForm('admin/structure/views/nojs/add-handler/test_groupwise_term_ui/default/relationship', $edit, 'Add and configure relationships');
// Apply changes.
$edit = array();
$this->drupalPostForm('admin/structure/views/nojs/handler/test_groupwise_term_ui/default/relationship/tid_representative', $edit, 'Apply');
}
}

View file

@ -0,0 +1,69 @@
<?php
namespace Drupal\views_ui\Tests;
use Drupal\simpletest\WebTestBase;
use Drupal\views\Entity\View;
use Drupal\views\Views;
/**
* Tests the views list.
*
* @group views_ui
*/
class ViewsListTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('block', 'views_ui');
/**
* A user with permission to administer views.
*
* @var \Drupal\user\Entity\User
*/
protected $adminUser;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->drupalPlaceBlock('local_tasks_block');
$this->drupalPlaceBlock('local_actions_block');
$this->adminUser = $this->drupalCreateUser(['administer views']);
$this->drupalLogin($this->adminUser);
}
/**
* Tests that the views list does not use a pager.
*/
public function testViewsListLimit() {
// Check if we can access the main views admin page.
$this->drupalGet('admin/structure/views');
$this->assertResponse(200);
$this->assertLink(t('Add view'));
// Count default views to be subtracted from the limit.
$views = count(Views::getEnabledViews());
// Create multiples views.
$limit = 51;
$values = $this->config('views.view.test_view_storage')->get();
for ($i = 1; $i <= $limit - $views; $i++) {
$values['id'] = 'test_view_storage_new' . $i;
unset($values['uuid']);
$created = View::create($values);
$created->save();
}
$this->drupalGet('admin/structure/views');
// Check that all the rows are listed.
$this->assertEqual(count($this->xpath('//tbody/tr[contains(@class,"views-ui-list-enabled")]')), $limit);
}
}

View file

@ -0,0 +1,111 @@
<?php
namespace Drupal\views_ui\Tests;
use Drupal\tour\Tests\TourTestBase;
use Drupal\language\Entity\ConfigurableLanguage;
/**
* Tests the Views UI tour.
*
* @group views_ui
*/
class ViewsUITourTest extends TourTestBase {
/**
* An admin user with administrative permissions for views.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* String translation storage object.
*
* @var \Drupal\locale\StringStorageInterface
*/
protected $localeStorage;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('views_ui', 'tour', 'language', 'locale');
protected function setUp() {
parent::setUp();
$this->adminUser = $this->drupalCreateUser(array('administer views', 'access tour'));
$this->drupalLogin($this->adminUser);
}
/**
* Tests views_ui tour tip availability.
*/
public function testViewsUiTourTips() {
// Create a basic view that shows all content, with a page and a block
// display.
$view['label'] = $this->randomMachineName(16);
$view['id'] = strtolower($this->randomMachineName(16));
$view['page[create]'] = 1;
$view['page[path]'] = $this->randomMachineName(16);
$this->drupalPostForm('admin/structure/views/add', $view, t('Save and edit'));
$this->assertTourTips();
}
/**
* Tests views_ui tour tip availability in a different language.
*/
public function testViewsUiTourTipsTranslated() {
$langcode = 'nl';
// Add a default locale storage for this test.
$this->localeStorage = $this->container->get('locale.storage');
// Add Dutch language programmatically.
ConfigurableLanguage::createFromLangcode($langcode)->save();
// Handler titles that need translations.
$handler_titles = array(
'Format',
'Fields',
'Sort criteria',
'Filter criteria',
);
foreach ($handler_titles as $handler_title) {
// Create source string.
$source = $this->localeStorage->createString(array(
'source' => $handler_title
));
$source->save();
$this->createTranslation($source, $langcode);
}
// Create a basic view that shows all content, with a page and a block
// display.
$view['label'] = $this->randomMachineName(16);
$view['id'] = strtolower($this->randomMachineName(16));
$view['page[create]'] = 1;
$view['page[path]'] = $this->randomMachineName(16);
// Load the page in dutch.
$this->drupalPostForm(
$langcode . '/admin/structure/views/add',
$view,
t('Save and edit')
);
$this->assertTourTips();
}
/**
* Creates single translation for source string.
*/
public function createTranslation($source, $langcode) {
return $this->localeStorage->createTranslation(array(
'lid' => $source->lid,
'language' => $langcode,
'translation' => $this->randomMachineName(100),
))->save();
}
}

View file

@ -0,0 +1,62 @@
<?php
namespace Drupal\views_ui\Tests;
use Drupal\views\Tests\Wizard\WizardTestBase;
/**
* Tests the wizard.
*
* @group views_ui
* @see \Drupal\views\Plugin\views\display\DisplayPluginBase
* @see \Drupal\views\Plugin\views\display\PathPluginBase
* @see \Drupal\views\Plugin\views\wizard\WizardPluginBase
*/
class WizardTest extends WizardTestBase {
/**
* Tests filling in the wizard with really long strings.
*/
public function testWizardFieldLength() {
$view = array();
$view['label'] = $this->randomMachineName(256);
$view['id'] = strtolower($this->randomMachineName(129));
$view['page[create]'] = TRUE;
$view['page[path]'] = $this->randomMachineName(255);
$view['page[title]'] = $this->randomMachineName(256);
$view['page[feed]'] = TRUE;
$view['page[feed_properties][path]'] = $this->randomMachineName(255);
$view['block[create]'] = TRUE;
$view['block[title]'] = $this->randomMachineName(256);
$view['rest_export[create]'] = TRUE;
$view['rest_export[path]'] = $this->randomMachineName(255);
$this->drupalPostForm('admin/structure/views/add', $view, t('Save and edit'));
$this->assertText('Machine-readable name cannot be longer than 128 characters but is currently 129 characters long.');
$this->assertText('Path cannot be longer than 254 characters but is currently 255 characters long.');
$this->assertText('Page title cannot be longer than 255 characters but is currently 256 characters long.');
$this->assertText('View name cannot be longer than 255 characters but is currently 256 characters long.');
$this->assertText('Feed path cannot be longer than 254 characters but is currently 255 characters long.');
$this->assertText('Block title cannot be longer than 255 characters but is currently 256 characters long.');
$this->assertText('REST export path cannot be longer than 254 characters but is currently 255 characters long.');
$view['label'] = $this->randomMachineName(255);
$view['id'] = strtolower($this->randomMachineName(128));
$view['page[create]'] = TRUE;
$view['page[path]'] = $this->randomMachineName(254);
$view['page[title]'] = $this->randomMachineName(255);
$view['page[feed]'] = TRUE;
$view['page[feed_properties][path]'] = $this->randomMachineName(254);
$view['block[create]'] = TRUE;
$view['block[title]'] = $this->randomMachineName(255);
$view['rest_export[create]'] = TRUE;
$view['rest_export[path]'] = $this->randomMachineName(254);
$this->drupalPostForm('admin/structure/views/add', $view, t('Save and edit'));
$this->assertUrl('admin/structure/views/view/' . $view['id'], array(), 'Make sure the view saving was successful and the browser got redirected to the edit page.');
// Assert that the page title is correctly truncated.
$this->assertText(views_ui_truncate($view['page[title]'], 32));
}
}

View file

@ -0,0 +1,45 @@
<?php
namespace Drupal\views_ui\Tests;
/**
* Tests the Xss vulnerability.
*
* @group views_ui
*/
class XssTest extends UITestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('node', 'user', 'views_ui', 'views_ui_test');
public function testViewsUi() {
$this->drupalGet('admin/structure/views');
$this->assertEscaped('<script>alert("foo");</script>, <marquee>test</marquee>', 'The view tag is properly escaped.');
$this->drupalGet('admin/structure/views/view/sa_contrib_2013_035');
$this->assertEscaped('<marquee>test</marquee>', 'Field admin label is properly escaped.');
$this->drupalGet('admin/structure/views/nojs/handler/sa_contrib_2013_035/page_1/header/area');
$this->assertEscaped('{{ title }} == <marquee>test</marquee>', 'Token label is properly escaped.');
$this->assertEscaped('{{ title_1 }} == <script>alert("XSS")</script>', 'Token label is properly escaped.');
}
/**
* Checks the admin UI for double escaping.
*/
public function testNoDoubleEscaping() {
$this->drupalGet('admin/structure/views');
$this->assertNoEscaped('&lt;');
$this->drupalGet('admin/structure/views/view/sa_contrib_2013_035');
$this->assertNoEscaped('&lt;');
$this->drupalGet('admin/structure/views/nojs/handler/sa_contrib_2013_035/page_1/header/area');
$this->assertNoEscaped('&lt;');
}
}

View file

@ -0,0 +1,206 @@
<?php
namespace Drupal\views_ui;
use Drupal\Core\Form\FormStateInterface;
use Drupal\views\Plugin\views\wizard\WizardPluginBase;
use Drupal\views\Plugin\views\wizard\WizardException;
use Drupal\views\Plugin\ViewsPluginManager;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Form controller for the Views edit form.
*/
class ViewAddForm extends ViewFormBase {
/**
* The wizard plugin manager.
*
* @var \Drupal\views\Plugin\ViewsPluginManager
*/
protected $wizardManager;
/**
* Constructs a new ViewEditForm object.
*
* @param \Drupal\views\Plugin\ViewsPluginManager $wizard_manager
* The wizard plugin manager.
*/
public function __construct(ViewsPluginManager $wizard_manager) {
$this->wizardManager = $wizard_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('plugin.manager.views.wizard')
);
}
/**
* {@inheritdoc}
*/
protected function prepareEntity() {
// Do not prepare the entity while it is being added.
}
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
$form['#attached']['library'][] = 'views_ui/views_ui.admin';
$form['#attributes']['class'] = array('views-admin');
$form['name'] = array(
'#type' => 'fieldset',
'#title' => t('View basic information'),
'#attributes' => array('class' => array('fieldset-no-legend')),
);
$form['name']['label'] = array(
'#type' => 'textfield',
'#title' => $this->t('View name'),
'#required' => TRUE,
'#size' => 32,
'#default_value' => '',
'#maxlength' => 255,
);
$form['name']['id'] = array(
'#type' => 'machine_name',
'#maxlength' => 128,
'#machine_name' => array(
'exists' => '\Drupal\views\Views::getView',
'source' => array('name', 'label'),
),
'#description' => $this->t('A unique machine-readable name for this View. It must only contain lowercase letters, numbers, and underscores.'),
);
$form['name']['description_enable'] = array(
'#type' => 'checkbox',
'#title' => $this->t('Description'),
);
$form['name']['description'] = array(
'#type' => 'textfield',
'#title' => $this->t('Provide description'),
'#title_display' => 'invisible',
'#size' => 64,
'#default_value' => '',
'#states' => array(
'visible' => array(
':input[name="description_enable"]' => array('checked' => TRUE),
),
),
);
// 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',
'#title' => t('View settings'),
'#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 = $this->wizardManager->getDefinitions();
$options = array();
foreach ($wizard_plugins as $key => $wizard) {
$options[$key] = $wizard['title'];
}
$form['displays']['show']['wizard_key'] = array(
'#type' => 'select',
'#title' => $this->t('Show'),
'#options' => $options,
);
$show_form = &$form['displays']['show'];
$default_value = \Drupal::moduleHandler()->moduleExists('node') ? 'node' : 'users';
$show_form['wizard_key']['#default_value'] = WizardPluginBase::getSelected($form_state, array('show', 'wizard_key'), $default_value, $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'];
$wizard_instance = $this->wizardManager->createInstance($wizard_key);
$form = $wizard_instance->buildForm($form, $form_state);
return $form;
}
/**
* {@inheritdoc}
*/
protected function actions(array $form, FormStateInterface $form_state) {
$actions = parent::actions($form, $form_state);
$actions['submit']['#value'] = $this->t('Save and edit');
// Remove EntityFormController::save() form the submission handlers.
$actions['submit']['#submit'] = array(array($this, 'submitForm'));
$actions['cancel'] = array(
'#type' => 'submit',
'#value' => $this->t('Cancel'),
'#submit' => array('::cancel'),
'#limit_validation_errors' => array(),
);
return $actions;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
$wizard_type = $form_state->getValue(array('show', 'wizard_key'));
$wizard_instance = $this->wizardManager->createInstance($wizard_type);
$form_state->set('wizard', $wizard_instance->getPluginDefinition());
$form_state->set('wizard_instance', $wizard_instance);
$errors = $wizard_instance->validateView($form, $form_state);
foreach ($errors as $display_errors) {
foreach ($display_errors as $name => $message) {
$form_state->setErrorByName($name, $message);
}
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
try {
/** @var $wizard \Drupal\views\Plugin\views\wizard\WizardInterface */
$wizard = $form_state->get('wizard_instance');
$this->entity = $wizard->createView($form, $form_state);
}
// @todo Figure out whether it really makes sense to throw and catch exceptions on the wizard.
catch (WizardException $e) {
drupal_set_message($e->getMessage(), 'error');
$form_state->setRedirect('entity.view.collection');
return;
}
$this->entity->save();
drupal_set_message($this->t('The view %name has been saved.', array('%name' => $form_state->getValue('label'))));
$form_state->setRedirectUrl($this->entity->urlInfo('edit-form'));
}
/**
* Form submission handler for the 'cancel' action.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
public function cancel(array $form, FormStateInterface $form_state) {
$form_state->setRedirect('entity.view.collection');
}
}

View file

@ -0,0 +1,78 @@
<?php
namespace Drupal\views_ui;
use Drupal\Core\Form\FormStateInterface;
/**
* Form controller for the Views duplicate form.
*/
class ViewDuplicateForm extends ViewFormBase {
/**
* {@inheritdoc}
*/
protected function prepareEntity() {
// Do not prepare the entity while it is being added.
}
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
parent::form($form, $form_state);
$form['#title'] = $this->t('Duplicate of @label', array('@label' => $this->entity->label()));
$form['label'] = array(
'#type' => 'textfield',
'#title' => $this->t('View name'),
'#required' => TRUE,
'#size' => 32,
'#maxlength' => 255,
'#default_value' => $this->t('Duplicate of @label', array('@label' => $this->entity->label())),
);
$form['id'] = array(
'#type' => 'machine_name',
'#maxlength' => 128,
'#machine_name' => array(
'exists' => '\Drupal\views\Views::getView',
'source' => array('label'),
),
'#default_value' => '',
'#description' => $this->t('A unique machine-readable name for this View. It must only contain lowercase letters, numbers, and underscores.'),
);
return $form;
}
/**
* {@inheritdoc}
*/
protected function actions(array $form, FormStateInterface $form_state) {
$actions['submit'] = array(
'#type' => 'submit',
'#value' => $this->t('Duplicate'),
);
return $actions;
}
/**
* Form submission handler for the 'clone' action.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* A reference to a keyed array containing the current state of the form.
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->entity = $this->entity->createDuplicate();
$this->entity->set('label', $form_state->getValue('label'));
$this->entity->set('id', $form_state->getValue('id'));
$this->entity->save();
// Redirect the user to the view admin form.
$form_state->setRedirectUrl($this->entity->urlInfo('edit-form'));
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,170 @@
<?php
namespace Drupal\views_ui;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Form\FormStateInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
/**
* Base form for Views forms.
*/
abstract class ViewFormBase extends EntityForm {
/**
* The name of the display used by the form.
*
* @var string
*/
protected $displayID;
/**
* {@inheritdoc}
*/
public function init(FormStateInterface $form_state) {
parent::init($form_state);
// @todo Remove the need for this.
$form_state->loadInclude('views_ui', 'inc', 'admin');
$form_state->set('view', $this->entity);
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, $display_id = NULL) {
if (isset($display_id) && $form_state->has('display_id') && ($display_id !== $form_state->get('display_id'))) {
throw new \InvalidArgumentException('Mismatch between $form_state->get(\'display_id\') and $display_id.');
}
$this->displayID = $form_state->has('display_id') ? $form_state->get('display_id') : $display_id;
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
protected function prepareEntity() {
// Determine the displays available for editing.
if ($tabs = $this->getDisplayTabs($this->entity)) {
if (empty($this->displayID)) {
// If a display isn't specified, use the first one after sorting by
// #weight.
uasort($tabs, 'Drupal\Component\Utility\SortArray::sortByWeightProperty');
foreach ($tabs as $id => $tab) {
if (!isset($tab['#access']) || $tab['#access']) {
$this->displayID = $id;
break;
}
}
}
// If a display is specified, but we don't have access to it, return
// an access denied page.
if ($this->displayID && !isset($tabs[$this->displayID])) {
throw new NotFoundHttpException();
}
elseif ($this->displayID && (isset($tabs[$this->displayID]['#access']) && !$tabs[$this->displayID]['#access'])) {
throw new AccessDeniedHttpException();
}
}
elseif ($this->displayID) {
throw new NotFoundHttpException();
}
}
/**
* 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 $display_id
* The display_id which is edited on the current request.
*/
public function getDisplayTabs(ViewUI $view) {
$executable = $view->getExecutable();
$executable->initDisplay();
$display_id = $this->displayID;
$tabs = array();
// Create a tab for each display.
foreach ($view->get('display') as $id => $display) {
// Get an instance of the display plugin, to make sure it will work in the
// UI.
$display_plugin = $executable->displayHandlers->get($id);
if (empty($display_plugin)) {
continue;
}
$tabs[$id] = array(
'#theme' => 'menu_local_task',
'#weight' => $display['position'],
'#link' => array(
'title' => $this->getDisplayLabel($view, $id),
'localized_options' => array(),
'url' => $view->urlInfo('edit-display-form')->setRouteParameter('display_id', $id),
),
);
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 ((!$this->isDefaultDisplayShown($view) && $display_id != 'default') && count($tabs) > 1) {
$tabs['default']['#access'] = FALSE;
}
// Mark the display tab as red to show validation errors.
$errors = $executable->validate();
foreach ($view->get('display') as $id => $display) {
if (!empty($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'] = $this->t('This display has one or more validation errors.');
}
}
return $tabs;
}
/**
* Controls whether or not the default display should have its own tab on edit.
*/
public function isDefaultDisplayShown(ViewUI $view) {
// Always show the default display for advanced users who prefer that mode.
$advanced_mode = \Drupal::config('views.settings')->get('ui.show.master_display');
// 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->getExecutable()->displayHandlers) == 1);
return $advanced_mode || $additional_displays;
}
/**
* Placeholder function for overriding $display['display_title'].
*
* @todo Remove this function once editing the display title is possible.
*/
public function getDisplayLabel(ViewUI $view, $display_id, $check_changed = TRUE) {
$display = $view->get('display');
$title = $display_id == 'default' ? $this->t('Master') : $display[$display_id]['display_title'];
$title = views_ui_truncate($title, 25);
if ($check_changed && !empty($view->changed_display[$display_id])) {
$changed = '*';
$title = $title . $changed;
}
return $title;
}
}

View file

@ -0,0 +1,287 @@
<?php
namespace Drupal\views_ui;
use Drupal\Component\Plugin\PluginManagerInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Config\Entity\ConfigEntityListBuilder;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines a class to build a listing of view entities.
*
* @see \Drupal\views\Entity\View
*/
class ViewListBuilder extends ConfigEntityListBuilder {
/**
* The views display plugin manager to use.
*
* @var \Drupal\Component\Plugin\PluginManagerInterface
*/
protected $displayManager;
/**
* {@inheritdoc}
*/
protected $limit;
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$entity_type,
$container->get('entity.manager')->getStorage($entity_type->id()),
$container->get('plugin.manager.views.display')
);
}
/**
* Constructs a new ViewListBuilder object.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
* @param \Drupal\Core\Entity\EntityStorageInterface $storage.
* The entity storage class.
* @param \Drupal\Component\Plugin\PluginManagerInterface $display_manager
* The views display plugin manager to use.
*/
public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage, PluginManagerInterface $display_manager) {
parent::__construct($entity_type, $storage);
$this->displayManager = $display_manager;
// This list builder uses client-side filters which requires all entities to
// be listed, disable the pager.
// @todo https://www.drupal.org/node/2536826 change the filtering to support
// a pager.
$this->limit = FALSE;
}
/**
* {@inheritdoc}
*/
public function load() {
$entities = array(
'enabled' => array(),
'disabled' => array(),
);
foreach (parent::load() as $entity) {
if ($entity->status()) {
$entities['enabled'][] = $entity;
}
else {
$entities['disabled'][] = $entity;
}
}
return $entities;
}
/**
* {@inheritdoc}
*/
public function buildRow(EntityInterface $view) {
$row = parent::buildRow($view);
return array(
'data' => array(
'view_name' => array(
'data' => array(
'#theme' => 'views_ui_view_info',
'#view' => $view,
'#displays' => $this->getDisplaysList($view)
),
),
'description' => array(
'data' => array(
'#plain_text' => $view->get('description'),
),
'data-drupal-selector' => 'views-table-filter-text-source',
),
'tag' => array(
'data' => array(
'#plain_text' => $view->get('tag'),
),
'data-drupal-selector' => 'views-table-filter-text-source',
),
'path' => array(
'data' => array(
'#theme' => 'item_list',
'#items' => $this->getDisplayPaths($view),
'#context' => ['list_style' => 'comma-list'],
),
),
'operations' => $row['operations'],
),
'title' => $this->t('Machine name: @name', array('@name' => $view->id())),
'class' => array($view->status() ? 'views-ui-list-enabled' : 'views-ui-list-disabled'),
);
}
/**
* {@inheritdoc}
*/
public function buildHeader() {
return array(
'view_name' => array(
'data' => $this->t('View name'),
'class' => array('views-ui-name'),
),
'description' => array(
'data' => $this->t('Description'),
'class' => array('views-ui-description'),
),
'tag' => array(
'data' => $this->t('Tag'),
'class' => array('views-ui-tag'),
),
'path' => array(
'data' => $this->t('Path'),
'class' => array('views-ui-path'),
),
'operations' => array(
'data' => $this->t('Operations'),
'class' => array('views-ui-operations'),
),
);
}
/**
* {@inheritdoc}
*/
public function getDefaultOperations(EntityInterface $entity) {
$operations = parent::getDefaultOperations($entity);
if ($entity->hasLinkTemplate('duplicate-form')) {
$operations['duplicate'] = array(
'title' => $this->t('Duplicate'),
'weight' => 15,
'url' => $entity->urlInfo('duplicate-form'),
);
}
// Add AJAX functionality to enable/disable operations.
foreach (array('enable', 'disable') as $op) {
if (isset($operations[$op])) {
$operations[$op]['url'] = $entity->urlInfo($op);
// Enable and disable operations should use AJAX.
$operations[$op]['attributes']['class'][] = 'use-ajax';
}
}
return $operations;
}
/**
* {@inheritdoc}
*/
public function render() {
$entities = $this->load();
$list['#type'] = 'container';
$list['#attributes']['id'] = 'views-entity-list';
$list['#attached']['library'][] = 'core/drupal.ajax';
$list['#attached']['library'][] = 'views_ui/views_ui.listing';
$form['filters'] = array(
'#type' => 'container',
'#attributes' => array(
'class' => array('table-filter', 'js-show'),
),
);
$list['filters']['text'] = array(
'#type' => 'search',
'#title' => $this->t('Filter'),
'#title_display' => 'invisible',
'#size' => 40,
'#placeholder' => $this->t('Filter by view name or description'),
'#attributes' => array(
'class' => array('views-filter-text'),
'data-table' => '.views-listing-table',
'autocomplete' => 'off',
'title' => $this->t('Enter a part of the view name or description to filter by.'),
),
);
$list['enabled']['heading']['#markup'] = '<h2>' . $this->t('Enabled', array(), array('context' => 'Plural')) . '</h2>';
$list['disabled']['heading']['#markup'] = '<h2>' . $this->t('Disabled', array(), array('context' => 'Plural')) . '</h2>';
foreach (array('enabled', 'disabled') as $status) {
$list[$status]['#type'] = 'container';
$list[$status]['#attributes'] = array('class' => array('views-list-section', $status));
$list[$status]['table'] = array(
'#type' => 'table',
'#attributes' => array(
'class' => array('views-listing-table'),
),
'#header' => $this->buildHeader(),
'#rows' => array(),
);
foreach ($entities[$status] as $entity) {
$list[$status]['table']['#rows'][$entity->id()] = $this->buildRow($entity);
}
}
// @todo Use a placeholder for the entity label if this is abstracted to
// other entity types.
$list['enabled']['table']['#empty'] = $this->t('There are no enabled views.');
$list['disabled']['table']['#empty'] = $this->t('There are no disabled views.');
return $list;
}
/**
* Gets a list of displays included in the view.
*
* @param \Drupal\Core\Entity\EntityInterface $view
* The view entity instance to get a list of displays for.
*
* @return array
* An array of display types that this view includes.
*/
protected function getDisplaysList(EntityInterface $view) {
$displays = array();
foreach ($view->get('display') as $display) {
$definition = $this->displayManager->getDefinition($display['display_plugin']);
if (!empty($definition['admin'])) {
// Cast the admin label to a string since it is an object.
// @see \Drupal\Core\StringTranslation\TranslatableMarkup
$displays[] = (string) $definition['admin'];
}
}
sort($displays);
return $displays;
}
/**
* Gets a list of paths assigned to the view.
*
* @param \Drupal\Core\Entity\EntityInterface $view
* The view entity.
*
* @return array
* An array of paths for this view.
*/
protected function getDisplayPaths(EntityInterface $view) {
$all_paths = array();
$executable = $view->getExecutable();
$executable->initDisplay();
foreach ($executable->displayHandlers as $display) {
if ($display->hasPath()) {
$path = $display->getPath();
if ($view->status() && strpos($path, '%') === FALSE) {
// @todo Views should expect and store a leading /. See:
// https://www.drupal.org/node/2423913
$all_paths[] = \Drupal::l('/' . $path, Url::fromUserInput('/' . $path));
}
else {
$all_paths[] = '/' . $path;
}
}
}
return array_unique($all_paths);
}
}

View file

@ -0,0 +1,106 @@
<?php
namespace Drupal\views_ui;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
/**
* Form controller for the Views preview form.
*/
class ViewPreviewForm extends ViewFormBase {
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
$view = $this->entity;
$form['#prefix'] = '<div id="views-preview-wrapper" class="views-preview-wrapper views-admin clearfix">';
$form['#suffix'] = '</div>';
$form['#id'] = 'views-ui-preview-form';
$form_state->disableCache();
$form['controls']['#attributes'] = array('class' => array('clearfix'));
$form['controls']['title'] = array(
'#prefix' => '<h2 class="view-preview-form__title">',
'#markup' => $this->t('Preview'),
'#suffix' => '</h2>',
);
// Add a checkbox controlling whether or not this display auto-previews.
$form['controls']['live_preview'] = array(
'#type' => 'checkbox',
'#id' => 'edit-displays-live-preview',
'#title' => $this->t('Auto preview'),
'#default_value' => \Drupal::config('views.settings')->get('ui.always_live_preview'),
);
// Add the arguments textfield
$form['controls']['view_args'] = array(
'#type' => 'textfield',
'#title' => $this->t('Preview with contextual filters:'),
'#description' => $this->t('Separate contextual filter values with a "/". For example, %example.', array('%example' => '40/12/10')),
'#id' => 'preview-args',
);
$args = array();
if (!$form_state->isValueEmpty('view_args')) {
$args = explode('/', $form_state->getValue('view_args'));
}
$user_input = $form_state->getUserInput();
if ($form_state->get('show_preview') || !empty($user_input['js'])) {
$form['preview'] = array(
'#weight' => 110,
'#theme_wrappers' => array('container'),
'#attributes' => array('id' => 'views-live-preview', 'class' => 'views-live-preview'),
'preview' => $view->renderPreview($this->displayID, $args),
);
}
$uri = $view->urlInfo('preview-form');
$uri->setRouteParameter('display_id', $this->displayID);
$form['#action'] = $uri->toString();
return $form;
}
/**
* {@inheritdoc}
*/
protected function actions(array $form, FormStateInterface $form_state) {
$view = $this->entity;
return array(
'#attributes' => array(
'id' => 'preview-submit-wrapper',
'class' => array('preview-submit-wrapper')
),
'button' => array(
'#type' => 'submit',
'#value' => $this->t('Update preview'),
'#attributes' => array('class' => array('arguments-preview')),
'#submit' => array('::submitPreview'),
'#id' => 'preview-submit',
'#ajax' => array(
'url' => Url::fromRoute('entity.view.preview_form', ['view' => $view->id(), 'display_id' => $this->displayID]),
'wrapper' => 'views-preview-wrapper',
'event' => 'click',
'progress' => array('type' => 'fullscreen'),
'method' => 'replaceWith',
'disable-refocus' => TRUE,
),
),
);
}
/**
* Form submission handler for the Preview button.
*/
public function submitPreview($form, FormStateInterface $form_state) {
$form_state->set('show_preview', TRUE);
$form_state->setRebuild();
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,57 @@
{#
/**
* @file
* Default theme implementation for Views UI build group filter form.
*
* Available variables:
* - form: A render element representing the form. Contains the following:
* - form_description: The exposed filter's description.
* - expose_button: The button to toggle the expose filter form.
* - group_button: Toggle options between single and grouped filters.
* - label: A filter label input field.
* - description: A filter description field.
* - value: The filters available values.
* - optional: A checkbox to require this filter or not.
* - remember: A checkbox to remember selected filter value(s) (per user).
* - widget: Radio Buttons to select the filter widget.
* - add_group: A button to add another row to the table.
* - more: A details element for additional field exposed filter fields.
* - table: A rendered table element of the group filter form.
*
* @see template_preprocess_views_ui_build_group_filter_form()
*
* @ingroup themeable
*/
#}
{{ form.form_description }}
{{ form.expose_button }}
{{ form.group_button }}
<div class="views-left-40">
{{ form.optional }}
{{ form.remember }}
</div>
<div class="views-right-60">
{{ form.widget }}
{{ form.label }}
{{ form.description }}
</div>
{#
Render the rest of the form elements excluding elements that are rendered
elsewhere.
#}
{{ form|without(
'form_description',
'expose_button',
'group_button',
'optional',
'remember',
'widget',
'label',
'description',
'add_group',
'more'
)
}}
{{ table }}
{{ form.add_group }}
{{ form.more }}

View file

@ -0,0 +1,13 @@
{#
/**
* @file
* Default theme implementation for a generic views UI container/wrapper.
*
* Available variables:
* - attributes: HTML attributes to apply to the container element.
* - children: The remaining elements such as dropbuttons and tabs.
*
* @ingroup themeable
*/
#}
<div{{ attributes }}>{{ children }}</div>

View file

@ -0,0 +1,35 @@
{#
/**
* @file
* Default theme implementation for each "box" on the display query edit screen.
*
* Available variables:
* - attributes: HTML attributes to apply to the container element.
* - actions: Action links such as "Add", "And/Or, Rearrange" for the content.
* - title: The title of the bucket, e.g. "Fields", "Filter Criteria", etc.
* - content: Content items such as fields or settings in this container.
* - name: The name of the bucket, e.g. "Fields", "Filter Criteria", etc.
* - overridden: A boolean indicating the setting has been overridden from the
* default.
*
* @see template_preprocess_views_ui_display_tab_bucket()
*
* @ingroup themeable
*/
#}
{%
set classes = [
'views-ui-display-tab-bucket',
name ? name|clean_class,
overridden ? 'overridden',
]
%}
<div{{ attributes.addClass(classes) }}>
{% if title -%}
<h3 class="views-ui-display-tab-bucket__title">{{ title }}</h3>
{%- endif %}
{% if actions -%}
{{ actions }}
{%- endif %}
{{ content }}
</div>

View file

@ -0,0 +1,37 @@
{#
/**
* @file
* Default theme implementation for Views UI display tab settings.
*
* Template for each row inside the "boxes" on the display query edit screen.
*
* Available variables:
* - attributes: HTML attributes such as class for the container.
* - description: The description or label for this setting.
* - settings_links: A list of links for this setting.
* - defaulted: A boolean indicating the setting is in its default state.
* - overridden: A boolean indicating the setting has been overridden from the
* default.
*
* @see template_preprocess_views_ui_display_tab_setting()
*
* @ingroup themeable
*/
#}
{%
set classes = [
'views-display-setting',
'clearfix',
'views-ui-display-tab-setting',
defaulted ? 'defaulted',
overridden ? 'overridden',
]
%}
<div{{ attributes.addClass(classes) }}>
{% if description -%}
<span class="label">{{ description }}</span>
{%- endif %}
{% if settings_links %}
{{ settings_links|safe_join('<span class="label">&nbsp;|&nbsp;</span>') }}
{% endif %}
</div>

View file

@ -0,0 +1,67 @@
{#
/**
* @file
* Default theme implementation for exposed filter form.
*
* Available variables:
* - form_description: The exposed filter's description.
* - expose_button: The button to toggle the expose filter form.
* - group_button: Toggle options between single and grouped filters.
* - required: A checkbox to require this filter or not.
* - label: A filter label input field.
* - description: A filter description field.
* - operator: The operators for how the filters value should be treated.
* - #type: The operator type.
* - value: The filters available values.
* - use_operator: Checkbox to allow the user to expose the operator.
* - more: A details element for additional field exposed filter fields.
*
* @ingroup themeable
*/
#}
{{ form.form_description }}
{{ form.expose_button }}
{{ form.group_button }}
{{ form.required }}
{{ form.label }}
{{ form.description }}
{{ form.operator }}
{{ form.value }}
{% if form.use_operator %}
<div class="views-left-40">
{{ form.use_operator }}
</div>
{% endif %}
{#
Collect a list of elements printed to exclude when printing the
remaining elements.
#}
{% set remaining_form = form|without(
'form_description',
'expose_button',
'group_button',
'required',
'label',
'description',
'operator',
'value',
'use_operator',
'more'
)
%}
{#
Only output the right column markup if there's a left column to begin with.
#}
{% if form.operator['#type'] %}
<div class="views-right-60">
{{ remaining_form }}
</div>
{% else %}
{{ remaining_form }}
{% endif %}
{{ form.more }}

View file

@ -0,0 +1,27 @@
{#
/**
* @file
* Default theme implementation for Views UI rearrange filter form.
*
* Available variables:
* - form: A render element representing the form.
* - grouping: A flag whether or not there is more than one group.
* - ungroupable_table: The ungroupable filter table.
* - table: The groupable filter table.
*
* @see template_preprocess_views_ui_rearrange_filter_form()
*
* @ingroup themeable
*/
#}
{{ form.override }}
<div class="scroll" data-drupal-views-scroll>
{% if grouping %}
{{ form.filter_groups.operator }}
{% else %}
{{ form.filter_groups.groups.0 }}
{% endif %}
{{ ungroupable_table }}
{{ table }}
</div>
{{ form|without('override', 'filter_groups', 'remove_groups', 'filters') }}

View file

@ -0,0 +1,18 @@
{#
/**
* @file
* Default template for the settings of a table style views display.
*
* Available variables:
* - table: A table of options for each field in this display.
* - form: Any remaining form fields not included in the table.
* - description_markup: An overview for the settings of this display.
*
* @see template_preprocess_views_ui_style_plugin_table()
*
* @ingroup themeable
*/
#}
{{ form.description_markup }}
{{ table }}
{{ form }}

View file

@ -0,0 +1,28 @@
{#
/**
* @file
* Default theme implementation for basic administrative info about a View.
*
* Available variables:
* - displays: List of displays.
*
* @ingroup themeable
*/
#}
<h3 class="views-ui-view-title" data-drupal-selector="views-table-filter-text-source">{{ view.label }}</h3>
<div class="views-ui-view-displays">
{% if displays %}
{% trans %}
Display
{% plural displays %}
Displays
{% endtrans %}:
<em>{{ displays|safe_join(', ') }}</em>
{% else %}
{{ 'None'|t }}
{% endif %}
</div>
<div class="views-ui-view-machine-name">
{{ 'Machine name:'|t }}
<span data-drupal-selector="views-table-filter-text-source">{{ view.id }}</span>
</div>

View file

@ -0,0 +1,20 @@
{#
/**
* @file
* Default theme implementation for a views UI preview section.
*
* Available variables:
* - title: The human readable section title.
* - links: A list of contextual links.
* - content: The content for this section preview.
*
* @see template_preprocess_views_ui_view_preview_section()
*
* @ingroup themeable
*/
#}
<h1 class="section-title">{{ title }}</h1>
{% if links %}
<div class="contextual">{{ links }}</div>
{% endif %}
<div class="preview-section">{{ content }}</div>

View file

@ -0,0 +1,187 @@
langcode: en
status: true
dependencies:
module:
- node
- user
id: sa_contrib_2013_035
label: SA_CONTRIB_2013_035
module: views
description: ''
tag: '<script>alert("foo");</script>, <marquee>test</marquee>'
base_table: node_field_data
base_field: nid
core: 8.x
display:
default:
display_plugin: default
id: default
display_title: Master
position: 0
display_options:
access:
type: perm
options:
perm: 'access content'
cache:
type: tag
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: none
options:
offset: 0
style:
type: default
options:
grouping: { }
row_class: ''
default_row_class: true
uses_fields: false
row:
type: fields
options:
inline: { }
separator: ''
hide_empty: false
default_field_elements: true
fields:
title:
id: title
table: node_field_data
field: title
relationship: none
group_type: group
admin_label: '<marquee>test</marquee>'
label: ''
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: false
ellipsis: false
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
plugin_id: field
entity_type: node
entity_field: title
title_1:
id: title_1
table: node_field_data
field: title
relationship: none
group_type: group
admin_label: '<script>alert("XSS")</script>'
label: ''
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
plugin_id: field
entity_type: node
entity_field: title
filters: { }
sorts: { }
header:
area:
id: area
table: views
field: area
plugin_id: text
footer: { }
empty: { }
relationships: { }
arguments: { }
title: '<marquee>VIEWS TITLE</marquee>'
page_1:
display_plugin: page
id: page_1
display_title: Page
position: 2
display_options:
path: foobar

View file

@ -0,0 +1,4 @@
/**
* @file
* Just a placeholder file for the test.
*/

View file

@ -0,0 +1,8 @@
name: 'Views UI Test'
type: module
description: 'Test module for Views UI.'
package: Testing
version: VERSION
core: 8.x
dependencies:
- views_ui

View file

@ -0,0 +1,4 @@
views_ui_test.test:
css:
component:
css/views_ui_test.test.css: {}

View file

@ -0,0 +1,17 @@
<?php
/**
* @file
* Helper module for Views UI tests.
*/
/**
* Implements hook_views_preview_info_alter().
*
* Add a row count row to the live preview area.
*/
function views_ui_test_views_preview_info_alter(&$rows, $view) {
$data = ['#markup' => t('Test row count')];
$data['#attached']['library'][] = 'views_ui_test/views_ui_test.test';
$rows['query'][] = [['data' => $data], count($view->result)];
}

View file

@ -0,0 +1,76 @@
<?php
namespace Drupal\Tests\views_ui\FunctionalJavascript;
use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
/**
* Tests the View UI filter criteria group dialog.
*
* @group views_ui
*/
class FilterCriteriaTest extends JavascriptTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['node', 'views', 'views_ui'];
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
$admin_user = $this->drupalCreateUser([
'administer site configuration',
'administer views',
'administer nodes',
'access content overview',
]);
// Disable automatic live preview to make the sequence of calls clearer.
\Drupal::configFactory()->getEditable('views.settings')->set('ui.always_live_preview', FALSE)->save();
$this->drupalLogin($admin_user);
}
/**
* Tests dialog for filter criteria.
*/
public function testFilterCriteriaDialog() {
$this->drupalGet('admin/structure/views/view/content_recent');
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
// Use the 'And/Or Rearrange' link for fields to open a dialog.
$dropbutton = $page->find('css', '.views-ui-display-tab-bucket.filter .dropbutton-toggle button');
$dropbutton->click();
$add_link = $page->findById('views-rearrange-filter');
$this->assertTrue($add_link->isVisible(), 'And/Or Rearrange button found.');
$add_link->click();
$assert_session->assertWaitOnAjaxRequest();
// Add a new filter group.
$create_new_filter_group = $page->findById('views-add-group-link');
$this->assertTrue($create_new_filter_group->isVisible(), 'Add group link found.');
$create_new_filter_group->click();
$assert_session->assertWaitOnAjaxRequest();
// Assert the existence of the new filter group by checking the remove group
// link.
$remove_link = $page->findLink('Remove group');
$this->assertTrue($remove_link->isVisible(), 'New group found.');
// Remove the group again and assert the group is not present anymore.
$remove_link->click();
$assert_session->assertWaitOnAjaxRequest();
$remove_link = $page->findLink('Remove group');
$this->assertEmpty($remove_link, 'Remove button not available');
// Checks that the admin summary is not double escaped.
$this->drupalGet('admin/structure/views/view/who_s_online');
$page = $this->getSession()->getPage();
$this->assertNotNull($page->findLink('User: Last access (>= -15 minutes)'));
}
}

View file

@ -0,0 +1,69 @@
<?php
namespace Drupal\Tests\views_ui\FunctionalJavascript;
use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
/**
* Tests the JavaScript library caching on consecutive requests.
*
* @group views_ui
*/
class LibraryCachingTest extends JavascriptTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['node', 'views', 'views_ui'];
/**
* Tests if the Views UI dialogs open on consecutive requests.
*/
public function testConsecutiveDialogRequests() {
$admin_user = $this->drupalCreateUser([
'administer site configuration',
'administer views',
'administer nodes',
'access content overview',
]);
// Disable automatic live preview to make the sequence of calls clearer.
\Drupal::configFactory()->getEditable('views.settings')->set('ui.always_live_preview', FALSE)->save();
$this->drupalLogin($admin_user);
$this->drupalGet('admin/structure/views/view/content');
$page = $this->getSession()->getPage();
// Use the 'Add' link for fields to open a dialog. This will load the proper
// dialog libraries.
$add_link = $page->findById('views-add-field');
$this->assertTrue($add_link->isVisible(), 'Add fields button found.');
$add_link->click();
$this->assertJsCondition("jQuery('.ui-dialog-titlebar').length > 0");
// Close the dialog and open it again. No no libraries will be loaded, but a
// cache entry will be made for not loading any libraries.
$page->pressButton('Close');
$add_link->click();
$this->assertJsCondition("jQuery('.ui-dialog-titlebar').length > 0");
$page->pressButton('Close');
// Reload the page.
$this->drupalGet('admin/structure/views/view/content');
$page = $this->getSession()->getPage();
// Now use the 'Update preview' button to load libraries.
$preview = $page->findById('preview-submit');
// The first click will load all the libraries.
$preview->click();
$this->assertJsCondition("jQuery('.ajax-progress').length === 0");
// The second click will not load any new libraries.
$preview->click();
$this->assertJsCondition("jQuery('.ajax-progress').length === 0");
// Check to see if the dialogs still open.
$add_link = $page->findById('views-add-field');
$add_link->click();
$this->assertJsCondition("jQuery('.ui-dialog-titlebar').length > 0");
$page->pressButton('Close');
}
}

View file

@ -0,0 +1,123 @@
<?php
namespace Drupal\Tests\views_ui\FunctionalJavascript;
use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
/**
* Tests the JavaScript filtering on the Views listing page.
*
* @see core/modules/views_ui/js/views_ui.listing.js
* @group views_ui
*/
class ViewsListingTest extends JavascriptTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['node', 'views', 'views_ui'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$admin_user = $this->drupalCreateUser([
'administer site configuration',
'administer views',
]);
$this->drupalLogin($admin_user);
}
/**
* Tests the filtering on the Views listing page.
*/
public function testFilterViewsListing() {
$this->drupalGet('admin/structure/views');
$session = $this->assertSession();
$page = $this->getSession()->getPage();
// Test that we search in both the enabled and disabled rows.
$enabled_rows = $page->findAll('css', 'tr.views-ui-list-enabled');
$enabled_rows = $this->filterVisibleElements($enabled_rows);
$disabled_rows = $page->findAll('css', 'tr.views-ui-list-disabled');
$disabled_rows = $this->filterVisibleElements($disabled_rows);
// Test that we see some rows of views in both tables.
$this->assertCount(6, $enabled_rows);
$this->assertCount(2, $disabled_rows);
// Filter on the string 'people'. This should only show the people view.
$search_input = $page->find('css', '.views-filter-text.form-search');
$search_input->setValue('people');
$enabled_rows = $page->findAll('css', 'tr.views-ui-list-enabled');
$enabled_rows = $this->filterVisibleElements($enabled_rows);
$disabled_rows = $page->findAll('css', 'tr.views-ui-list-disabled');
$disabled_rows = $this->filterVisibleElements($disabled_rows);
$this->assertCount(1, $enabled_rows);
$this->assertCount(0, $disabled_rows);
// Filter on a string that also appears in the description.
$search_input->setValue('content');
$enabled_rows = $page->findAll('css', 'tr.views-ui-list-enabled');
$enabled_rows = $this->filterVisibleElements($enabled_rows);
$disabled_rows = $page->findAll('css', 'tr.views-ui-list-disabled');
$disabled_rows = $this->filterVisibleElements($disabled_rows);
$this->assertCount(3, $enabled_rows);
$this->assertCount(2, $disabled_rows);
// Reset the search string and check that we are back to the initial stage.
$search_input->setValue('');
// Add a backspace to trigger the keyUp event.
$search_input->keyUp(8);
$enabled_rows = $page->findAll('css', 'tr.views-ui-list-enabled');
$enabled_rows = $this->filterVisibleElements($enabled_rows);
$disabled_rows = $page->findAll('css', 'tr.views-ui-list-disabled');
$disabled_rows = $this->filterVisibleElements($disabled_rows);
$this->assertCount(6, $enabled_rows);
$this->assertCount(2, $disabled_rows);
// Disable a View and see if it moves to the disabled listing.
$enabled_view = $page->find('css', 'tr.views-ui-list-enabled');
// Open the dropdown with additional actions.
$enabled_view->find('css', 'li.dropbutton-toggle button')->click();
$disable_button = $enabled_view->find('css', 'li.disable.dropbutton-action a');
// Check that the disable button is visible now.
$this->assertTrue($disable_button->isVisible());
$disable_button->click();
$session->assertWaitOnAjaxRequest();
$enabled_rows = $page->findAll('css', 'tr.views-ui-list-enabled');
$enabled_rows = $this->filterVisibleElements($enabled_rows);
$disabled_rows = $page->findAll('css', 'tr.views-ui-list-disabled');
$disabled_rows = $this->filterVisibleElements($disabled_rows);
// Test that one enabled View has been moved to the disabled list.
$this->assertCount(5, $enabled_rows);
$this->assertCount(3, $disabled_rows);
}
/**
* Removes any non-visible elements from the passed array.
*
* @param array $elements
* @return array
*/
protected function filterVisibleElements($elements) {
$elements = array_filter($elements, function($element) {
return $element->isVisible();
});
return $elements;
}
}

View file

@ -0,0 +1,82 @@
<?php
namespace Drupal\Tests\views_ui\FunctionalJavascript;
use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
/**
* Tests views creation wizard.
*
* @see core/modules/views_ui/js/views-admin.js
* @group views_ui
*/
class ViewsWizardTest extends JavascriptTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['node', 'views', 'views_ui', 'block', 'user'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$admin_user = $this->drupalCreateUser([
'administer site configuration',
'administer views',
]);
$this->drupalLogin($admin_user);
}
/**
* Tests creating a View using the wizard.
*/
public function testCreateViewWizard() {
$this->drupalGet('admin/structure/views/add');
$page = $this->getSession()->getPage();
// Set a view name, this should be used to prepopulate a number of other
// fields when creating displays.
$label_value = 'test view';
$search_input = $page->findField('label');
$search_input->setValue($label_value);
$page->findField('page[create]')->click();
// Test if the title and path have been populated.
$this->assertEquals($label_value, $page->findField('page[title]')->getValue());
$this->assertEquals(str_replace(' ', '-', $label_value), $page->findField('page[path]')->getValue());
// Create a menu item.
$page->findField('page[link]')->click();
$this->assertEquals($label_value, $page->findField('page[link_properties][title]')->getValue());
// Add a block display.
$page->findField('block[create]')->click();
$this->assertEquals($label_value, $page->findField('block[title]')->getValue());
// Select the entity type to display and test that the type selector is
// shown when expected.
$page->selectFieldOption('show[wizard_key]', 'node');
$this->assertSession()->assertWaitOnAjaxRequest();
$this->assertNull($page->findField('show[type]'), 'The "of type" filter is not added for nodes when there are no node types.');
$this->assertEquals('teasers', $page->findField('page[style][row_plugin]')->getValue(), 'The page display format shows the expected default value.');
$this->assertEquals('titles_linked', $page->findField('block[style][row_plugin]')->getValue(), 'The block display format shows the expected default value.');
$page->selectFieldOption('show[wizard_key]', 'users');
$this->assertSession()->assertWaitOnAjaxRequest();
$this->assertNull($page->findField('show[type]'), 'The "of type" filter is not added for users.');
$this->assertEquals('fields', $page->findField('page[style][row_plugin]')->getValue(), 'The page display format was updated to a valid value.');
$this->assertEquals('fields', $page->findField('block[style][row_plugin]')->getValue(), 'The block display format was updated to a valid value.');
$this->drupalCreateContentType(['type' => 'page']);
$page->selectFieldOption('show[wizard_key]', 'node');
$this->assertSession()->assertWaitOnAjaxRequest();
$this->assertNotNull($page->findField('show[type]'), 'The "of type" filter is added for nodes when there is at least one node type.');
$this->assertEquals('fields', $page->findField('page[style][row_plugin]')->getValue(), 'The page display format was not changed from a valid value.');
$this->assertEquals('fields', $page->findField('block[style][row_plugin]')->getValue(), 'The block display format was not changed from a valid value.');
}
}

View file

@ -0,0 +1,72 @@
<?php
namespace Drupal\Tests\views_ui\Kernel;
use Drupal\Tests\views\Kernel\ViewsKernelTestBase;
use Drupal\views_ui\Controller\ViewsUIController;
use Drupal\Component\Utility\Html;
use Drupal\views\Entity\View;
/**
* Tests the views ui tagging functionality.
*
* @group views_ui
*/
class TagTest extends ViewsKernelTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('views', 'views_ui', 'user');
/**
* Tests the views_ui_autocomplete_tag function.
*/
public function testViewsUiAutocompleteTag() {
\Drupal::moduleHandler()->loadInclude('views_ui', 'inc', 'admin');
// Save 15 views with a tag.
$tags = array();
for ($i = 0; $i < 16; $i++) {
$suffix = $i % 2 ? 'odd' : 'even';
$tag = 'autocomplete_tag_test_' . $suffix . $this->randomMachineName();
$tags[] = $tag;
View::create(array('tag' => $tag, 'id' => $this->randomMachineName()))->save();
}
// Make sure just ten results are returns.
$controller = ViewsUIController::create($this->container);
$request = $this->container->get('request_stack')->getCurrentRequest();
$request->query->set('q', 'autocomplete_tag_test');
$result = $controller->autocompleteTag($request);
$matches = (array) json_decode($result->getContent(), TRUE);
$this->assertEqual(count($matches), 10, 'Make sure the maximum amount of tag results is 10.');
// Make sure the returned array has the proper format.
$suggestions = array_map(function ($tag) {
return array('value' => $tag, 'label' => Html::escape($tag));
}, $tags);
foreach ($matches as $match) {
$this->assertTrue(in_array($match, $suggestions), 'Make sure the returned array has the proper format.');
}
// Make sure that matching by a certain prefix works.
$request->query->set('q', 'autocomplete_tag_test_even');
$result = $controller->autocompleteTag($request);
$matches = (array) json_decode($result->getContent(), TRUE);
$this->assertEqual(count($matches), 8, 'Make sure that only a subset is returned.');
foreach ($matches as $tag) {
$this->assertTrue(array_search($tag['value'], $tags) !== FALSE, format_string('Make sure the returned tag @tag actually exists.', array('@tag' => $tag['value'])));
}
// Make sure an invalid result doesn't return anything.
$request->query->set('q', $this->randomMachineName());
$result = $controller->autocompleteTag($request);
$matches = (array) json_decode($result->getContent());
$this->assertEqual(count($matches), 0, "Make sure an invalid tag doesn't return anything.");
}
}

View file

@ -0,0 +1,25 @@
<?php
namespace Drupal\Tests\views_ui\Unit\Form\Ajax;
use Drupal\Tests\UnitTestCase;
use Drupal\views_ui\Form\Ajax\RearrangeFilter;
/**
* Unit tests for Views UI module functions.
*
* @group views_ui
*/
class RearrangeFilterTest extends UnitTestCase {
/**
* Tests static methods.
*/
public function testStaticMethods() {
// Test the RearrangeFilter::arrayKeyPlus method.
$original = array(0 => 'one', 1 => 'two', 2 => 'three');
$expected = array(1 => 'one', 2 => 'two', 3 => 'three');
$this->assertSame(RearrangeFilter::arrayKeyPlus($original), $expected);
}
}

View file

@ -0,0 +1,180 @@
<?php
/**
* @file
* Contains \Drupal\Tests\views_ui\Unit\ViewListBuilderTest.
*/
namespace Drupal\Tests\views_ui\Unit;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Tests\UnitTestCase;
use Drupal\views\Entity\View;
use Drupal\views\ViewExecutableFactory;
use Drupal\views_ui\ViewListBuilder;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* @coversDefaultClass \Drupal\views_ui\ViewListBuilder
* @group views_ui
*/
class ViewListBuilderTest extends UnitTestCase {
/**
* Tests the listing of displays on a views list builder.
*
* @see \Drupal\views_ui\ViewListBuilder::getDisplaysList()
* @covers ::buildRow
*/
public function testBuildRowEntityList() {
$storage = $this->getMockBuilder('Drupal\Core\Config\Entity\ConfigEntityStorage')
->disableOriginalConstructor()
->getMock();
$display_manager = $this->getMockBuilder('\Drupal\views\Plugin\ViewsPluginManager')
->disableOriginalConstructor()
->getMock();
$display_manager->expects($this->any())
->method('getDefinition')
->will($this->returnValueMap(array(
array(
'default',
TRUE,
array(
'id' => 'default',
'title' => 'Master',
'theme' => 'views_view',
'no_ui' => TRUE,
'admin' => '',
)
),
array(
'page',
TRUE,
array(
'id' => 'page',
'title' => 'Page',
'uses_menu_links' => TRUE,
'uses_route' => TRUE,
'contextual_links_locations' => array('page'),
'theme' => 'views_view',
'admin' => 'Page admin label',
)
),
array(
'embed',
TRUE,
array(
'id' => 'embed',
'title' => 'embed',
'theme' => 'views_view',
'admin' => 'Embed admin label',
)
),
)));
$default_display = $this->getMock('Drupal\views\Plugin\views\display\DefaultDisplay',
array('initDisplay'),
array(array(), 'default', $display_manager->getDefinition('default'))
);
$route_provider = $this->getMock('Drupal\Core\Routing\RouteProviderInterface');
$state = $this->getMock('\Drupal\Core\State\StateInterface');
$menu_storage = $this->getMock('\Drupal\Core\Entity\EntityStorageInterface');
$page_display = $this->getMock('Drupal\views\Plugin\views\display\Page',
array('initDisplay', 'getPath'),
array(array(), 'default', $display_manager->getDefinition('page'), $route_provider, $state, $menu_storage)
);
$page_display->expects($this->any())
->method('getPath')
->will($this->onConsecutiveCalls(
$this->returnValue('test_page'),
$this->returnValue('<object>malformed_path</object>'),
$this->returnValue('<script>alert("placeholder_page/%")</script>')));
$embed_display = $this->getMock('Drupal\views\Plugin\views\display\Embed', array('initDisplay'),
array(array(), 'default', $display_manager->getDefinition('embed'))
);
$values = array();
$values['status'] = FALSE;
$values['display']['default']['id'] = 'default';
$values['display']['default']['display_title'] = 'Display';
$values['display']['default']['display_plugin'] = 'default';
$values['display']['page_1']['id'] = 'page_1';
$values['display']['page_1']['display_title'] = 'Page 1';
$values['display']['page_1']['display_plugin'] = 'page';
$values['display']['page_1']['display_options']['path'] = 'test_page';
$values['display']['page_2']['id'] = 'page_2';
$values['display']['page_2']['display_title'] = 'Page 2';
$values['display']['page_2']['display_plugin'] = 'page';
$values['display']['page_2']['display_options']['path'] = '<object>malformed_path</object>';
$values['display']['page_3']['id'] = 'page_3';
$values['display']['page_3']['display_title'] = 'Page 3';
$values['display']['page_3']['display_plugin'] = 'page';
$values['display']['page_3']['display_options']['path'] = '<script>alert("placeholder_page/%")</script>';
$values['display']['embed']['id'] = 'embed';
$values['display']['embed']['display_title'] = 'Embedded';
$values['display']['embed']['display_plugin'] = 'embed';
$display_manager->expects($this->any())
->method('createInstance')
->will($this->returnValueMap(array(
array('default', $values['display']['default'], $default_display),
array('page', $values['display']['page_1'], $page_display),
array('page', $values['display']['page_2'], $page_display),
array('page', $values['display']['page_3'], $page_display),
array('embed', $values['display']['embed'], $embed_display),
)));
$container = new ContainerBuilder();
$user = $this->getMock('Drupal\Core\Session\AccountInterface');
$request_stack = new RequestStack();
$request_stack->push(new Request());
$views_data = $this->getMockBuilder('Drupal\views\ViewsData')
->disableOriginalConstructor()
->getMock();
$route_provider = $this->getMock('Drupal\Core\Routing\RouteProviderInterface');
$executable_factory = new ViewExecutableFactory($user, $request_stack, $views_data, $route_provider);
$container->set('views.executable', $executable_factory);
$container->set('plugin.manager.views.display', $display_manager);
\Drupal::setContainer($container);
// Setup a view list builder with a mocked buildOperations method,
// because t() is called on there.
$entity_type = $this->getMock('Drupal\Core\Entity\EntityTypeInterface');
$view_list_builder = new TestViewListBuilder($entity_type, $storage, $display_manager);
$view_list_builder->setStringTranslation($this->getStringTranslationStub());
$view = new View($values, 'view');
$row = $view_list_builder->buildRow($view);
$expected_displays = array(
'Embed admin label',
'Page admin label',
'Page admin label',
'Page admin label',
);
$this->assertEquals($expected_displays, $row['data']['view_name']['data']['#displays']);
$display_paths = $row['data']['path']['data']['#items'];
// These values will be escaped by Twig when rendered.
$this->assertEquals('/test_page, /<object>malformed_path</object>, /<script>alert("placeholder_page/%")</script>', implode(', ', $display_paths));
}
}
class TestViewListBuilder extends ViewListBuilder {
public function buildOperations(EntityInterface $entity) {
return array();
}
}

View file

@ -0,0 +1,141 @@
<?php
namespace Drupal\Tests\views_ui\Unit;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Tests\UnitTestCase;
use Drupal\views\Entity\View;
use Drupal\views_ui\ViewUI;
use Symfony\Component\DependencyInjection\ContainerBuilder;
/**
* @coversDefaultClass \Drupal\views_ui\ViewUI
* @group views_ui
*/
class ViewUIObjectTest extends UnitTestCase {
/**
* Tests entity method decoration.
*/
public function testEntityDecoration() {
$method_args = array();
$method_args['setOriginalId'] = array(12);
$method_args['setStatus'] = array(TRUE);
$method_args['enforceIsNew'] = array(FALSE);
$method_args['label'] = array(LanguageInterface::LANGCODE_NOT_SPECIFIED);
$reflection = new \ReflectionClass('Drupal\Core\Config\Entity\ConfigEntityInterface');
$interface_methods = array();
foreach ($reflection->getMethods() as $reflection_method) {
$interface_methods[] = $reflection_method->getName();
// EntityInterface::isNew() is missing from the list of methods, because it
// calls id(), which breaks the ->expect($this->once()) call. Call it later.
// EntityInterface::isSyncing() is only called during syncing process.
// EntityInterface::isUninstalling() is only called during uninstallation
// process. EntityInterface::getConfigDependencyName() and
// ConfigEntityInterface::calculateDependencies() are only used for
// dependency management.
if (!in_array($reflection_method->getName(), ['isNew', 'isSyncing', 'isUninstalling', 'getConfigDependencyKey', 'getConfigDependencyName', 'calculateDependencies'])) {
if (count($reflection_method->getParameters()) == 0) {
$method_args[$reflection_method->getName()] = array();
}
}
}
$storage = $this->getMock('Drupal\views\Entity\View', $interface_methods, array(array(), 'view'));
$executable = $this->getMockBuilder('Drupal\views\ViewExecutable')
->disableOriginalConstructor()
->setConstructorArgs(array($storage))
->getMock();
$storage->set('executable', $executable);
$view_ui = new ViewUI($storage);
foreach ($method_args as $method => $args) {
$method_mock = $storage->expects($this->once())
->method($method);
foreach ($args as $arg) {
$method_mock->with($this->equalTo($arg));
}
call_user_func_array(array($view_ui, $method), $args);
}
$storage->expects($this->once())
->method('isNew');
$view_ui->isNew();
}
/**
* Tests the isLocked method.
*/
public function testIsLocked() {
$storage = $this->getMock('Drupal\views\Entity\View', array(), array(array(), 'view'));
$executable = $this->getMockBuilder('Drupal\views\ViewExecutable')
->disableOriginalConstructor()
->setConstructorArgs(array($storage))
->getMock();
$storage->set('executable', $executable);
$account = $this->getMock('Drupal\Core\Session\AccountInterface');
$account->expects($this->exactly(2))
->method('id')
->will($this->returnValue(1));
$container = new ContainerBuilder();
$container->set('current_user', $account);
\Drupal::setContainer($container);
$view_ui = new ViewUI($storage);
// A view_ui without a lock object is not locked.
$this->assertFalse($view_ui->isLocked());
// Set the lock object with a different owner than the mocked account above.
$lock = (object) array(
'owner' => 2,
'data' => array(),
'updated' => (int) $_SERVER['REQUEST_TIME'],
);
$view_ui->lock = $lock;
$this->assertTrue($view_ui->isLocked());
// Set a different lock object with the same object as the mocked account.
$lock = (object) array(
'owner' => 1,
'data' => array(),
'updated' => (int) $_SERVER['REQUEST_TIME'],
);
$view_ui->lock = $lock;
$this->assertFalse($view_ui->isLocked());
}
/**
* Tests serialization of the ViewUI object.
*/
public function testSerialization() {
// Set a container so the DependencySerializationTrait has it.
$container = new ContainerBuilder();
\Drupal::setContainer($container);
$storage = new View([], 'view');
$executable = $this->getMockBuilder('Drupal\views\ViewExecutable')
->disableOriginalConstructor()
->setConstructorArgs([$storage])
->getMock();
$storage->set('executable', $executable);
$view_ui = new ViewUI($storage);
// Make sure the executable is returned before serializing.
$this->assertInstanceOf('Drupal\views\ViewExecutable', $view_ui->getExecutable());
$serialized = serialize($view_ui);
// Make sure the ViewExecutable class is not found in the serialized string.
$this->assertSame(strpos($serialized, '"Drupal\views\ViewExecutable"'), FALSE);
$unserialized = unserialize($serialized);
$this->assertInstanceOf('Drupal\views_ui\ViewUI', $unserialized);
}
}

Some files were not shown because too many files have changed in this diff Show more