first commit

This commit is contained in:
2024-07-15 12:33:27 +02:00
commit ce50ae282b
22084 changed files with 2623791 additions and 0 deletions

320
core/modules/views_ui/admin.inc Executable file
View File

@@ -0,0 +1,320 @@
<?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', []);
$seen_buttons = &drupal_static(__FUNCTION__ . ':seen_buttons', []);
// 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] = [
'#type' => 'submit',
// Hide this button when JavaScript is enabled.
'#attributes' => ['class' => ['js-hide']],
'#submit' => ['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(['views_ui_add_limited_validation'], $element_info->getInfoProperty('submit', '#process', [])),
// 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', []), ['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 (['#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', [
'@title' => $button_title,
]);
$seen_buttons[$button_title] = 1;
}
else {
$wrapping_element[$button_key]['#value'] = t('Update "@title" choice (@number)', [
'@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 = [
'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'] = [$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 += [
'#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 array
* 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();
}
/**
* Adds an element to select either the default display or 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'] = [
'#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()) {
$arguments = $form['#title']->getArguments() + ['@current' => $form_progress['current'], '@total' => $form_progress['total']];
$form['#title'] = t('Configure @type @current of @total: @item', $arguments);
}
// The dropdown should not be added when :
// - this is the default display.
// - there is no default 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.default_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)', ['@display_type' => $current_display->getPluginId()]);
// Only display the revert option if we are in an overridden section.
if (!$section_defaulted) {
$display_dropdown['default_revert'] = t('Revert to default');
}
$form['override']['dropdown'] = [
'#type' => 'select',
// @todo Translators may need more context than this.
'#title' => t('For'),
'#options' => $display_dropdown,
];
if ($current_display->isDefaulted($section)) {
$form['override']['dropdown']['#default_value'] = 'defaults';
}
else {
$form['override']['dropdown']['#default_value'] = $display_id;
}
}
/**
* Creates the menu path for a standard AJAX form given the form state.
*
* @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,236 @@
/**
* @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-top: 0;
margin-bottom: 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-right: 3px;
margin-left: 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 {
overflow: visible;
margin: 0;
padding: 0;
border-bottom: 0 none;
}
.views-displays .tabs > li {
float: left; /* LTR */
padding: 0;
border-right: 0 none; /* LTR */
}
[dir="rtl"] .views-displays .tabs > li {
float: right;
border-right: 1px solid #bfbfbf;
border-left: 0 none;
}
.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;
}
.views-displays .tabs .add {
position: relative;
}
.views-displays .tabs .action-list {
position: absolute;
z-index: 50;
top: 23px;
left: 0; /* LTR */
margin: 0;
}
[dir="rtl"] .views-displays .tabs .action-list {
right: 0;
left: auto;
}
.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 */
width: 32%;
margin-bottom: 0;
margin-left: 2%; /* LTR */
}
[dir="rtl"] .views-display-columns > * {
float: right;
margin-right: 2%;
margin-left: 0;
}
.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 {
overflow: auto;
max-height: 200px;
}
.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;
}
/* JS moves Views action buttons under a secondary tabs container, which causes
a large layout shift. We mitigate this by using animations to temporarily hide
the buttons, but they will appear after a set amount of time just in case the JS
is loaded but does not properly run. */
@media (scripting: enabled) {
.views-tabs__action-list-button:not(.views-tabs--secondary *) {
animation-name: appear;
animation-duration: 0.1s;
/* Buttons will be hidden for the amount of time in the animation-delay if
not moved. Note this is the approximate time to download the views
aggregate CSS with slow 3G. */
animation-delay: 5s;
animation-iteration-count: 1;
animation-fill-mode: backwards;
}
}
@keyframes appear {
from {
display: none;
}
to {
display: unset;
}
}

View File

@@ -0,0 +1,855 @@
/**
* @file
* The .admin.theme.css file is intended to contain presentation declarations
* including images, borders, colors, and fonts.
*/
.views-admin .links {
margin: 0;
list-style: none outside none;
}
.views-admin a:hover {
text-decoration: none;
}
.box-padding {
padding-right: 12px;
padding-left: 12px;
}
.box-margin {
margin: 12px 12px 0 12px;
}
.views-admin .icon {
width: 16px;
height: 16px;
}
.views-admin .icon,
.views-admin .icon-text {
background-image: url(../images/sprites.png);
background-repeat: no-repeat;
background-attachment: scroll;
background-position: left top; /* LTR */
}
[dir="rtl"] .views-admin .icon,
[dir="rtl"] .views-admin .icon-text {
background-position: right top;
}
.views-admin a.icon {
border: 1px solid #ddd;
border-radius: 4px;
background:
linear-gradient(-90deg, #fff 0, #e8e8e8 100%) no-repeat,
repeat-y;
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 {
position: relative;
float: left; /* LTR */
}
[dir="rtl"] .views-admin span.icon {
float: right;
}
.views-admin .icon.compact {
display: block;
overflow: hidden;
text-indent: -9999px;
direction: ltr;
}
/* Targets any element with an icon -> text combo */
.views-admin .icon-text {
padding-left: 19px; /* LTR */
}
[dir="rtl"] .views-admin .icon-text {
padding-right: 19px;
padding-left: 0;
}
.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-top: 0;
padding-bottom: 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-right: 4px;
padding-left: 0;
}
.views-admin details details.container-inline {
margin-top: 1em;
margin-bottom: 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-right: 16px;
margin-left: 0;
}
/* 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-top: 6px;
margin-bottom: 6px;
}
.views-ui-view-name h3 {
margin: 0.25em 0;
font-weight: bold;
}
.view-changed {
margin-bottom: 21px;
}
.views-admin .unit-title {
margin-top: 18px;
margin-bottom: 0;
font-size: 15px;
line-height: 1.6154;
}
.views-ui-view-displays ul {
margin-left: 0; /* LTR */
padding-left: 0; /* LTR */
list-style: none;
}
[dir="rtl"] .views-ui-view-displays ul {
margin-right: 0;
margin-left: inherit;
padding-right: 0;
padding-left: inherit;
}
/* These header classes are ambiguous and should be scoped to th elements */
.views-ui-name {
width: 20%;
}
.views-ui-description {
width: 30%;
}
.views-ui-machine-name {
width: 15%;
}
.views-ui-displays {
width: 25%;
}
.views-ui-operations {
width: 10%;
}
/**
* 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;
overflow: hidden;
height: 1px;
}
.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 {
padding-left: 0.5em; /* LTR */
text-transform: uppercase;
font-weight: bold;
font-style: italic;
}
[dir="rtl"] .views-operator-label {
padding-right: 0.5em;
padding-left: 0;
}
.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-right: 0;
padding-left: 10px;
}
.views-displays {
padding-bottom: 36px;
border: 1px solid #ccc;
}
.views-display-top {
position: relative;
padding: 8px 8px 3px;
border-bottom: 1px solid #ccc;
background-color: #e1e2dc;
}
.views-display-top .tabs {
margin-right: 18em; /* LTR */
}
[dir="rtl"] .views-display-top .tabs {
margin-right: 0;
margin-left: 18em;
}
.views-display-top .tabs > li {
margin-right: 6px; /* LTR */
padding-left: 0; /* LTR */
}
[dir="rtl"] .views-display-top .tabs > li {
margin-right: 0.3em;
margin-left: 6px;
padding-right: 0;
}
.views-display-top .tabs > li:last-child {
margin-right: 0; /* LTR */
}
[dir="rtl"] .views-display-top .tabs > li:last-child {
margin-right: 0.3em;
margin-left: 0;
}
.form-edit .form-actions {
margin-top: 0;
padding: 8px 12px;
border-right: 1px solid #ccc;
border-bottom: 1px solid #ccc;
border-left: 1px solid #ccc;
background-color: #e1e2dc;
}
.views-displays .tabs.secondary {
margin-right: 200px; /* LTR */
border: 0;
}
[dir="rtl"] .views-displays .tabs.secondary {
margin-right: 0;
margin-left: 200px;
}
.views-displays .tabs.secondary li,
.views-displays .tabs.secondary li.is-active {
width: auto;
padding: 0;
border: 0;
background: transparent;
}
.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-right: 6px;
margin-left: 5px;
}
.views-displays .tabs.secondary .tabs__tab + .tabs__tab {
border-top: 0;
}
.views-displays .tabs li.tabs__tab:hover {
padding-left: 0; /* LTR */
border: 0;
}
[dir="rtl"] .views-displays .tabs li.tabs__tab:hover {
padding-right: 0;
}
.views-displays .tabs.secondary a {
display: inline-block;
padding: 3px 7px;
border: 1px solid #cbcbcb;
border-radius: 7px;
font-size: small;
line-height: 1.3333;
}
/* Display a red border if the display doesn't validate. */
.views-displays .tabs li.is-active a.is-active.error,
.views-displays .tabs .error {
padding: 1px 6px;
border: 2px solid #ed541d;
}
.views-displays .tabs a:focus {
text-decoration: underline;
outline: none;
}
.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 {
color: #fff;
background-color: #555;
}
.views-displays .tabs .open > a {
position: relative;
border-bottom: 1px solid transparent;
background-color: #f1f1f1;
}
.views-displays .tabs .open > a:hover {
color: #0074bd;
background-color: #f1f1f1;
}
.views-displays .tabs .action-list li {
padding: 2px 9px;
border-width: 0 1px;
border-style: solid;
border-color: #cbcbcb;
background-color: #f1f1f1;
}
.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 {
margin: 0;
padding: 0;
border: medium none;
background: none repeat scroll 0 0 transparent;
}
.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 {
position: relative;
margin: 0 0 15px 0;
padding-top: 4px;
padding-bottom: 4px;
border: 1px solid #f3f3f3;
line-height: 20px;
}
.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-right: 10px;
margin-left: 0;
}
.view-preview-form .form-item-view-args label {
float: left; /* LTR */
height: 6ex;
margin-right: 0.75em; /* LTR */
font-weight: normal;
}
[dir="rtl"] .view-preview-form .form-item-view-args label {
float: right;
margin-right: 0.2em;
margin-left: 0.75em;
}
.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 {
position: relative;
margin: 0;
padding-top: 4px;
border-bottom: 1px solid #f3f3f3;
line-height: 20px;
}
.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 {
margin: 0;
font-size: small;
}
.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-right: 0.2083em;
margin-left: 0.2083em;
}
.views-ui-display-tab-setting > span {
margin-left: 0.5em; /* LTR */
}
[dir="rtl"] .views-ui-display-tab-setting > span {
margin-right: 0.5em;
margin-left: 0;
}
/** 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 {
padding-bottom: 2px;
color: #666;
font-size: 12px;
}
.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-right: 0;
margin-left: 3px;
}
.views-edit-view {
margin-bottom: 15px;
}
.views-edit-view.disabled .views-displays {
background-color: #fff4f4;
}
.views-edit-view.disabled .views-display-column {
background: white;
}
/* 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-top: 0;
margin-bottom: 0;
}
.views-filterable-options .filterable-option .title {
cursor: pointer;
font-weight: bold;
}
.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:not(:empty) {
padding: 8px 13px;
background-color: #f3f4ee;
}
.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 details .item-list {
padding-left: 2em; /* LTR */
}
[dir="rtl"] .views-ui-dialog details .item-list {
padding-right: 2em;
padding-left: 0;
}
.views-ui-rearrange-filter-form table {
border-collapse: collapse;
}
.views-ui-rearrange-filter-form tr td[rowspan] {
border-width: 0 1px 1px 1px;
border-style: solid;
border-color: #cdcdcd;
}
.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-right: 0;
border-left: 1px solid #cdcdcd;
}
.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-field-identifier,
.form-item-options-expose-description {
margin-top: 6px;
margin-bottom: 6px;
margin-left: 18px; /* LTR */
}
[dir="rtl"] .form-item-options-expose-required,
[dir="rtl"] .form-item-options-expose-label,
[dir="rtl"] .form-item-options-expose-field-identifier,
[dir="rtl"] .form-item-options-expose-description {
margin-right: 18px;
margin-left: 0;
}
.views-preview-wrapper {
border: 1px solid #ccc;
}
.view-preview-form {
position: relative;
}
.view-preview-form__title {
margin-top: 0;
padding: 8px 12px;
border-bottom: 1px solid #ccc;
background-color: #e1e2dc;
}
.view-preview-form .form-item-live-preview {
position: absolute;
top: 3px;
right: 12px;
margin-top: 2px;
margin-left: 2px; /* LTR */
}
[dir="rtl"] .view-preview-form .form-item-live-preview {
right: auto;
left: 12px;
margin-right: 2px;
margin-left: 0;
}
.views-live-preview {
padding: 12px;
}
.views-live-preview .views-query-info {
overflow: auto;
}
.views-live-preview .section-title {
display: inline-block;
margin-top: 0;
margin-bottom: 0;
color: #818181;
font-size: 13px;
font-weight: normal;
line-height: 1.6154;
}
.views-live-preview .view > * {
margin-top: 18px;
}
.views-live-preview .preview-section {
margin: 0 -5px;
padding: 3px 5px;
border: 1px dashed #dedede;
}
.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 {
margin: 10px 0;
border-spacing: 0;
border-collapse: separate;
border-color: #ddd;
}
.views-query-info table tr {
background-color: #f9f9f9;
}
.views-query-info table th,
.views-query-info table td {
padding: 4px 10px;
color: #666;
}
.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;
z-index: 2;
top: -1px;
right: -5px; /* LTR */
}
[dir="rtl"].js .dropbutton-wrapper .dropbutton .dropbutton-action > .ajax-progress-throbber {
right: auto;
left: -5px;
}
.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;
top: 7px;
right: 12px; /* LTR */
}
[dir="rtl"] .views-display-top .dropbutton-wrapper {
right: auto;
left: 12px;
}
.views-display-top .dropbutton-wrapper .dropbutton-widget .dropbutton-action a {
width: auto;
}
.views-ui-display-tab-bucket .dropbutton-wrapper {
position: absolute;
top: 4px;
right: 5px; /* LTR */
}
[dir="rtl"] .views-ui-display-tab-bucket .dropbutton-wrapper {
right: auto;
left: 5px;
}
.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 {
margin-bottom: 0;
padding-left: 12px; /* LTR */
border: medium;
background: none;
font-family: inherit;
font-size: 12px;
}
[dir="rtl"] .views-ui-display-tab-actions .dropbutton-wrapper li a,
[dir="rtl"] .views-ui-display-tab-actions .dropbutton-wrapper input {
padding-right: 12px;
padding-left: 0.5em;
}
.views-ui-display-tab-actions .dropbutton-wrapper input:hover {
border: none;
background: 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 {
top: auto;
right: auto; /* LTR */
}
[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 {
right: auto; /* LTR */
min-width: 10em;
padding: 6px 6px 9px 6px;
border-radius: 0 4px 4px 4px; /* LTR */
}
[dir="rtl"] .contextual .contextual-links {
left: auto;
border-radius: 4px 0 4px 4px;
}
.contextual-links li a,
.contextual-links li span {
padding-top: 0.25em;
padding-right: 0.1667em; /* LTR */
padding-bottom: 0.25em;
}
[dir="rtl"] .contextual-links li a,
[dir="rtl"] .contextual-links li span {
padding-right: 0;
padding-left: 0.1667em;
}
.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-right: 1em;
padding-left: 0.1667em;
}
.contextual-links li a:hover {
background-color: #badbec;
}

View File

@@ -0,0 +1,19 @@
---
label: 'Adding a new display to an existing view'
related:
- views.overview
- views_ui.edit
---
{% set views_link_text %}{% trans %}Views{% endtrans %}{% endset %}
{% set views_link = render_var(help_route_link(views_link_text, 'entity.view.collection')) %}
{% set view_edit_topic = render_var(help_topic_link('views_ui.edit')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Add a new display to an existing view. This will allow you to display similar data to an existing view, using similar settings, in a new block, page, feed, etc.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}If you are not already editing your view, in the <em>Manage</em> administrative menu, navigate to <em>Structure</em> &gt; <em>{{ views_link }}</em>. Find the view you want to edit, and click its <em>Edit</em> link.{% endtrans %}</li>
<li>{% trans %}Under <em>Displays</em>, click <em>Add</em>.{% endtrans %}</li>
<li>{% trans %}In the pop-up list, click the link for the type of display you want to add; the most common types are <em>Page</em> and <em>Block</em>. The new display will be added to your view, and you will be editing that display.{% endtrans %}</li>
<li>{% trans %}Optionally, click the link next to <em>Display name</em> and enter a new name to be shown for this display in the administrative interface.{% endtrans %}</li>
<li>{% trans %}Follow the steps in {{ view_edit_topic }} to edit the other settings for the display.{% endtrans %}</li>
</ol>

View File

@@ -0,0 +1,28 @@
---
label: 'Adding a bulk operations form to a view'
related:
- views.overview
- views_ui.create
- user.overview
---
{% set views_link_text %}
{% trans %}Views{% endtrans %}
{% endset %}
{% set views = render_var(help_route_link(views_link_text, 'entity.view.collection')) %}
{% set views_permissions_link_text %}
{% trans %}Administer views{% endtrans %}
{% endset %}
{% set views_permissions = render_var(help_route_link(views_permissions_link_text, 'user.admin_permissions.module', {'modules': 'views_ui'})) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Add one or more existing actions as bulk operations to an existing table-style view. If you have the core Actions UI module installed, see the related topic "Configuring actions" for more information about actions.{% endtrans %}</p>
<h2>{% trans %}Who can edit views?{% endtrans %}</h2>
<p>{% trans %}The core Views UI module will need to be installed and you will need <em>{{ views_permissions }}</em> permission in order to edit a view.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>Structure</em> &gt; <em>{{ views }}</em>. A list of all views is shown.{% endtrans %}</li>
<li>{% trans %}Find the view that you would like to edit, and click <em>Edit</em> from the dropdown button. Note that bulk operations work best in a view with a Page display, and a Table format.{% endtrans %}</li>
<li>{% trans %}If there is not already an <em>Operations bulk form</em> in the <em>Fields</em> list for the view, click <em>Add</em> in the <em>Fields</em> section to add it. (The exact name of the bulk form field will vary, and may contain keywords like "bulk update", "form element" or "operations" -- not to be confused with <em>operations links</em>, which are applied to each item in a row.) If the bulk operations field already exists, click the field name to edit its settings.{% endtrans %}</li>
<li>{% trans %}Check the action(s) you want to make available in the <em>Selected actions</em> list and click <em>Apply (all displays)</em>.{% endtrans %}</li>
<li>{% trans %}Verify that the <em>Access</em> settings for the view are at least as restrictive as the permissions necessary to perform the bulk operations. People with permission to see the view, but who don't have permission to do the bulk operations, will experience problems.{% endtrans %}</li>
<li>{% trans %}Click <em>Save</em>. The action(s) will be available as bulk operations in the view.{% endtrans %}</li>
</ol>

View File

@@ -0,0 +1,20 @@
---
label: 'Creating a new view'
related:
- views.overview
- views_ui.edit
- views_ui.add_display
---
{% set views_link_text %}{% trans %}Views{% endtrans %}{% endset %}
{% set views_link = render_var(help_route_link(views_link_text, 'entity.view.collection')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Create a new view to list content or other items on your site.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>Structure</em> &gt; <em>{{ views_link }}</em>.{% endtrans %}</li>
<li>{% trans %}Click <em>Add view</em>.{% endtrans %}</li>
<li>{% trans %}In the <em>View name</em> field, enter a name for the view, which is how it will be listed in the administrative interface.{% endtrans %}</li>
<li>{% trans %}In <em>View settings</em> &gt; <em>Show</em>, select the base data type to display in your view. This cannot be changed later.{% endtrans %}</li>
<li>{% trans %}Optionally, select or enter filtering, sorting, and page/block display settings; these can be added or changed later.{% endtrans %}</li>
<li>{% trans %}Click <em>Save and edit</em>. Your view will be created; edit it following the steps in the related topics below.{% endtrans %}</li>
</ol>

View File

@@ -0,0 +1,25 @@
---
label: 'Editing an existing view display'
related:
- views.overview
- views_ui.add_display
---
{% set views_link_text %}{% trans %}Views{% endtrans %}{% endset %}
{% set views_link = render_var(help_route_link(views_link_text, 'entity.view.collection')) %}
{% set views_overview_topic = render_var(help_topic_link('views.overview')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Edit an existing view display, to modify what data is displayed or how it is displayed.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}If you are not already editing your view, in the <em>Manage</em> administrative menu, navigate to <em>Structure</em> &gt; <em>{{ views_link }}</em>. Find the view you want to edit, and click its <em>Edit</em> link.{% endtrans %}</li>
<li>{% trans %}Under <em>Displays</em>, click the display you want to edit.{% endtrans %}</li>
<li>{% trans %}Find the section whose settings you want to change, such as <em>Format</em> or <em>Filter criteria</em>. (See {{ views_overview_topic }} for more information.){% endtrans %}</li>
<li>{% trans %}For sections containing lists (such as <em>Fields</em> and <em>Filter criteria</em>), to modify or delete an existing item, click the name of the item. To add a new item, click <em>Add</em> in the drop-down list. To change the order of items, click <em>Rearrange</em> in the drop-down list.{% endtrans %}</li>
<li>{% trans %}For sections containing individual settings (such as <em>Title</em> and <em>Format</em>), there are often two links for each setting. The first link shows the current value; click that link to change the value. If there is a second link called <em>Settings</em>, click that link to change the settings details. For example, if your <em>Format</em> is currently shown as <em>Unformatted list</em>, click <em>Unformatted list</em> to switch to using a <em>Grid</em> or <em>Table</em> format. Click <em>Settings</em> next to your format type to change the settings for your chosen format.{% endtrans %}</li>
<li>{% trans %}When you have finished changing all the settings, verify that the display is correct by clicking <em>Update preview</em>. Return to editing settings if necessary.{% endtrans %}</li>
<li>{% trans %}When you have verified the display, click <em>Save</em>. Alternatively, if you have made mistakes and want to discard your changes, click <em>Cancel</em>.{% endtrans %}</li>
</ol>
<h2>{% trans %}Additional resources{% endtrans %}</h2>
<ul>
<li>{% trans %}<a href="https://www.drupal.org/docs/user_guide/en/views-chapter.html">Creating Listings with Views (Drupal User Guide)</a>{% endtrans %}</li>
</ul>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

295
core/modules/views_ui/js/ajax.js Executable file
View File

@@ -0,0 +1,295 @@
/**
* @file
* Handles AJAX submission and response in Views UI.
*/
(function ($, Drupal, drupalSettings) {
/**
* 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,
) {
const $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.
const $submitButtons = $(
once(
'views-ajax-submit',
$form.find('input[type=submit].js-form-submit, button.js-form-submit'),
),
);
$submitButtons.on('click mousedown', function () {
this.form.clk = this;
});
once('views-ajax-submit', $form).forEach((form) => {
const $form = $(form);
const elementSettings = {
url: response.url,
event: 'submit',
base: $form.attr('id'),
element: form,
};
const ajaxForm = Drupal.ajax(elementSettings);
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 {boolean} 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')[0].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,
) {
const 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.
const oldTitle = doc.title;
// Escape the site name, in case it has special characters in it, so we can
// use it in our regex.
const escapedSiteName = response.siteName.replace(
/[-[\]{}()*+?.,\\^$|#\s]/g,
'\\$&',
);
const re = new RegExp(`.+ (.) ${escapedSiteName}`);
doc.title = oldTitle.replace(
re,
`${response.title} $1 ${response.siteName}`,
);
document.querySelectorAll('h1.page-title').forEach((item) => {
item.textContent = 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(context) {
$(once('views-ajax', 'input#edit-displays-live-preview', context)).on(
'click',
function () {
if (this.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(context) {
$(once('views-ajax', '#views-tabset a')).on('click', function () {
const href = $(this).attr('href');
// Cut of #views-tabset.
const displayId = href.substring(11);
const viewsPreviewId = document.querySelector(
'#views-live-preview #preview-display-id',
);
if (viewsPreviewId) {
// Set the form element if it is present.
viewsPreviewId.value = displayId;
}
});
},
};
/**
* 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(context, settings) {
const baseElementSettings = {
event: 'click',
progress: { type: 'fullscreen' },
};
// Bind AJAX behaviors to all items showing the class.
once('views-ajax', 'a.views-ajax-link', context).forEach((link) => {
const $link = $(link);
const elementSettings = baseElementSettings;
elementSettings.base = $link.attr('id');
elementSettings.element = link;
// Set the URL to go to the anchor.
if ($link.attr('href')) {
elementSettings.url = $link.attr('href');
}
Drupal.ajax(elementSettings);
});
once('views-ajax', 'div#views-live-preview a').forEach((link) => {
const $link = $(link);
// We don't bind to links without a URL.
if (!$link.attr('href')) {
return true;
}
const elementSettings = baseElementSettings;
// Set the URL to go to the anchor.
elementSettings.url = $link.attr('href');
if (
!Drupal.Views.getPath(elementSettings.url).startsWith(
'admin/structure/views',
)
) {
return true;
}
elementSettings.wrapper = 'views-preview-wrapper';
elementSettings.method = 'replaceWith';
elementSettings.base = link.id;
elementSettings.element = link;
Drupal.ajax(elementSettings);
});
// 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.
once('views-ajax', 'div#views-live-preview input[type=submit]').forEach(
(submit) => {
const $submit = $(submit);
$submit.on('click', function () {
this.form.clk = this;
return true;
});
const elementSettings = baseElementSettings;
// Set the URL to go to the anchor.
elementSettings.url = $(submit.form).attr('action');
if (
!Drupal.Views.getPath(elementSettings.url).startsWith(
'admin/structure/views',
)
) {
return true;
}
elementSettings.wrapper = 'views-preview-wrapper';
elementSettings.method = 'replaceWith';
elementSettings.event = 'click';
elementSettings.base = submit.id;
elementSettings.element = submit;
Drupal.ajax(elementSettings);
},
);
},
};
})(jQuery, Drupal, drupalSettings);

View File

@@ -0,0 +1,94 @@
/**
* @file
* Views dialog behaviors.
*/
(function ($, Drupal, drupalSettings, bodyScrollLock) {
function handleDialogResize(e) {
const $modal = $(e.currentTarget);
const $viewsOverride = $modal.find('[data-drupal-views-offset]');
const $scroll = $modal.find('[data-drupal-views-scroll]');
let offset = 0;
let 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.each(function () {
Object.assign(this.style, {
overflow: 'visible',
height: 'auto',
});
});
modalHeight = $modal.height();
$viewsOverride.each(function () {
offset += $(this).outerHeight();
});
// Take internal padding into account.
const scrollOffset = $scroll.outerHeight() - $scroll.height();
$scroll.height(modalHeight - offset - scrollOffset);
// Reset scrolling properties.
$modal.each(function () {
this.style.overflow = 'hidden';
});
$scroll.each(function () {
this.style.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(context) {
$(once('viewsDialog', 'body')).on(
'dialogContentResize.viewsDialog',
'.ui-dialog-content',
handleDialogResize,
);
// When expanding details, make sure the modal is resized.
$(once('detailsUpdate', '.scroll', context)).on(
'click',
'summary',
(e) => {
e.currentTarget?.dispatchEvent(
new CustomEvent('dialogContentResize', { bubbles: true }),
);
},
);
},
detach(context, settings, trigger) {
if (trigger === 'unload') {
$(once.remove('viewsDialog', 'body')).off('.viewsDialog');
}
},
};
/**
* Binds a listener on dialog creation to handle Views modal scroll.
*
* @param {jQuery.Event} e
* The event triggered.
* @param {Drupal.dialog~dialogDefinition} dialog
* The dialog instance.
* @param {jQuery} $element
* The jQuery collection of the dialog element.
*/
window.addEventListener('dialog:aftercreate', (e) => {
const $element = $(e.target);
const $scroll = $element.find('.scroll');
if ($scroll.length) {
bodyScrollLock.unlock($element.get(0));
bodyScrollLock.lock($scroll.get(0));
}
});
})(jQuery, Drupal, drupalSettings, bodyScrollLock);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,57 @@
/**
* @file
* Views listing behaviors.
*/
(function ($, Drupal) {
/**
* 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(context, settings) {
const [input] = once('views-filter-text', 'input.views-filter-text');
if (!input) {
return;
}
const $table = $(input.getAttribute('data-table'));
let $rows;
function filterViewList(e) {
const query = e.target.value.toLowerCase();
function showViewRow(index, row) {
const sources = row.querySelectorAll(
'[data-drupal-selector="views-table-filter-text-source"]',
);
let sourcesConcat = '';
sources.forEach((item) => {
sourcesConcat += item.textContent;
});
const textMatch = sourcesConcat.toLowerCase().includes(query);
$(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 [
'command' => 'viewsSetForm',
'url' => $this->url,
];
}
}

View File

@@ -0,0 +1,225 @@
<?php
namespace Drupal\views_ui\Controller;
use Drupal\Component\Utility\Tags;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Link;
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\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;
}
/**
* Lists all instances of fields on any views.
*
* @return array
* The Views fields report page.
*/
public function reportFields() {
$views = $this->entityTypeManager()->getStorage('view')->loadMultiple();
// Fetch all fieldapi fields which are used in views
// Therefore search in all views, displays and handler-types.
$fields = [];
$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 = [t('Field name'), t('Used in')];
$rows = [];
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][] = Link::fromTextAndUrl($view, new Url('entity.view.edit_form', ['view' => $view]))->toString();
}
$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 = [
'#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 $view) {
$views[] = Link::fromTextAndUrl($view, new Url('entity.view.edit_form', ['view' => $view]))->toString();
}
unset($row['views']);
$row['views']['data'] = [
'#theme' => 'item_list',
'#items' => $views,
'#context' => ['list_style' => 'comma-list'],
];
}
// Sort rows by field name.
ksort($rows);
return [
'#type' => 'table',
'#header' => [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->entityTypeManager()->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 = [];
$string = $request->query->get('q');
// Get matches from default views.
$views = $this->entityTypeManager()->getStorage('view')->loadMultiple();
// Keep track of previously processed tags so they can be skipped.
$tags = [];
foreach ($views as $view) {
$view_tag = $view->get('tag');
foreach (Tags::explode($view_tag) as $tag) {
if ($tag && !in_array($tag, $tags, TRUE)) {
$tags[] = $tag;
if (mb_stripos($tag, $string) !== FALSE) {
$matches[] = ['value' => $tag, 'label' => Html::escape($tag)];
if (count($matches) >= 10) {
break 2;
}
}
}
}
}
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', ['display_id' => $display_id]);
$build['preview'] = $this->entityFormBuilder()->getForm($view, 'preview', ['display_id' => $display_id]);
return $build;
}
}

View File

@@ -0,0 +1,101 @@
<?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.
*
* @internal
*/
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'] = [
'#type' => 'details',
'#title' => $this->t('Caching'),
'#open' => TRUE,
];
$form['cache']['clear_cache'] = [
'#type' => 'submit',
'#value' => $this->t("Clear Views' cache"),
'#submit' => ['::cacheSubmit'],
];
$form['debug'] = [
'#type' => 'details',
'#title' => $this->t('Debugging'),
'#open' => TRUE,
];
$form['debug']['sql_signature'] = [
'#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'] = [
'#type' => 'details',
'#title' => $this->t('Display extenders'),
'#open' => TRUE,
];
$form['extenders']['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('sql_signature', $form_state->getValue('sql_signature'))
->set('display_extenders', $form_state->getValue('display_extenders', []))
->save();
parent::submitForm($form, $form_state);
}
/**
* Submission handler to clear the Views cache.
*/
public function cacheSubmit() {
views_invalidate_cache();
$this->messenger()->addStatus($this->t('The cache has been cleared.'));
}
}

View File

@@ -0,0 +1,189 @@
<?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.
*
* @internal
*/
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 = [
'options' => [
'#theme_wrappers' => ['container'],
'#attributes' => ['class' => ['scroll'], 'data-drupal-views-scroll' => TRUE],
],
];
$executable = $view->getExecutable();
if (!$executable->setDisplay($display_id)) {
$form['markup'] = ['#markup' => $this->t('Invalid display id @display', ['@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', ['@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'] = [
'#theme_wrappers' => ['container'],
'#id' => 'views-filterable-options-controls',
'#attributes' => ['class' => ['form--inline', 'views-filterable-options-controls']],
];
$form['override']['controls']['options_search'] = [
'#type' => 'textfield',
'#title' => $this->t('Search'),
];
$groups = ['all' => $this->t('- All -')];
$form['override']['controls']['group'] = [
'#type' => 'select',
'#title' => $this->t('Category'),
'#options' => [],
];
$form['options']['name'] = [
'#prefix' => '<div class="views-radio-box form-checkboxes views-filterable-options">',
'#suffix' => '</div>',
'#type' => 'tableselect',
'#header' => [
'title' => $this->t('Title'),
'group' => $this->t('Category'),
'help' => $this->t('Description'),
],
'#js_select' => FALSE,
];
$grouped_options = [];
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] = [
'#attributes' => [
'class' => ['filterable-option', $group],
],
'title' => [
'data' => [
'#title' => $option['title'],
'#plain_text' => $option['title'],
],
'class' => ['title'],
],
'group' => $option['group'],
'help' => [
'data' => $option['help'],
'class' => ['description'],
],
];
}
}
$form['override']['controls']['group']['#options'] = $groups;
}
else {
$form['options']['markup'] = [
'#markup' => '<div class="js-form-item form-item">' . $this->t('There are no @types available to add.', ['@types' => $ltitle]) . '</div>',
];
}
// Add a div to show the selected items
$form['selected'] = [
'#type' => 'item',
'#markup' => '<span class="views-ui-view-title">' . $this->t('Selected:') . '</span> ' . '<div class="views-selected-options"></div>',
'#theme_wrappers' => ['form_element', 'views_ui_container'],
'#attributes' => [
'class' => ['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', ['@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'][] = [$view, 'submitItemAdd'];
return $form;
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace Drupal\views_ui\Form\Ajax;
use Drupal\Core\Form\FormStateInterface;
use Drupal\views\Views;
/**
* Displays analysis information for a view.
*
* @internal
*/
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'] = [
'#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 \Drupal\views_ui\ViewUI $view */
$view = $form_state->get('view');
$form_state->setRedirectUrl($view->toUrl('edit-form'));
}
}

View File

@@ -0,0 +1,281 @@
<?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.
*
* @internal
*/
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 = [
'options' => [
'#tree' => TRUE,
'#theme_wrappers' => ['container'],
'#attributes' => ['class' => ['scroll'], 'data-drupal-views-scroll' => TRUE],
],
];
$executable = $view->getExecutable();
$save_ui_cache = FALSE;
if (!$executable->setDisplay($display_id)) {
$form['markup'] = ['#markup' => $this->t('Invalid display id @display', ['@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'] = ['#markup' => $this->t("Error: handler for @table > @field doesn't exist!", ['@table' => $item['table'], '@field' => $item['field']])];
}
else {
$types = ViewExecutable::getHandlerTypes();
$form['#title'] = $this->t('Configure @type: @item', ['@type' => $types[$type]['lstitle'], '@item' => $handler->adminLabel()]);
// 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 = [];
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(['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'] = [
'#type' => 'select',
'#title' => $this->t('Relationship'),
'#options' => $relationship_options,
'#default_value' => $rel,
'#weight' => -500,
];
}
else {
$form['options']['relationship'] = [
'#type' => 'value',
'#value' => 'none',
];
}
if (!empty($handler->definition['help'])) {
$form['options']['form_description'] = [
'#markup' => $handler->definition['help'],
'#theme_wrappers' => ['container'],
'#attributes' => ['class' => ['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'] = [
'#type' => 'submit',
'#value' => $this->t('Remove'),
'#submit' => [[$this, 'remove']],
'#limit_validation_errors' => [['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
[$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,122 @@
<?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.
*
* @internal
*/
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 = [
'options' => [
'#tree' => TRUE,
'#theme_wrappers' => ['container'],
'#attributes' => ['class' => ['scroll'], 'data-drupal-views-scroll' => TRUE],
],
];
$executable = $view->getExecutable();
if (!$executable->setDisplay($display_id)) {
$form['markup'] = ['#markup' => $this->t('Invalid display id @display', ['@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'] = ['#markup' => $this->t("Error: handler for @table > @field doesn't exist!", ['@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', ['@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,115 @@
<?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.
*
* @internal
*/
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 = [
'options' => [
'#tree' => TRUE,
'#theme_wrappers' => ['container'],
'#attributes' => ['class' => ['scroll'], 'data-drupal-views-scroll' => TRUE],
],
];
$executable = $view->getExecutable();
if (!$executable->setDisplay($display_id)) {
$form['markup'] = ['#markup' => $this->t('Invalid display id @display', ['@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'] = ['#markup' => $this->t("Error: handler for @table > @field doesn't exist!", ['@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', ['@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,116 @@
<?php
namespace Drupal\views_ui\Form\Ajax;
use Drupal\Core\Form\FormStateInterface;
use Drupal\views\ViewEntityInterface;
/**
* Provides a form for editing the Views display.
*
* @internal
*/
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'] = ['#markup' => $this->t('Invalid display id @display', ['@display' => $display_id])];
return $form;
}
// Get form from the handler.
$form['options'] = [
'#theme_wrappers' => ['container'],
'#attributes' => ['class' => ['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,92 @@
<?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.
*
* @internal
*/
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'] = [
'#theme_wrappers' => ['container'],
'#attributes' => ['class' => ['scroll'], 'data-drupal-views-scroll' => TRUE],
];
$form['details']['label'] = [
'#type' => 'textfield',
'#title' => $this->t('Administrative name'),
'#default_value' => $view->label(),
];
$form['details']['langcode'] = [
'#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'] = [
'#type' => 'textfield',
'#title' => $this->t('Administrative description'),
'#default_value' => $view->get('description'),
];
$form['details']['tag'] = [
'#type' => 'textfield',
'#title' => $this->t('Administrative tags'),
'#description' => $this->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,186 @@
<?php
namespace Drupal\views_ui\Form\Ajax;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Link;
use Drupal\Core\Render\Markup;
use Drupal\Core\Url;
use Drupal\views\ViewEntityInterface;
use Drupal\views\ViewExecutable;
/**
* Provides a rearrange form for Views handlers.
*
* @internal
*/
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'] = ['#markup' => $this->t('Invalid display id @display', ['@display' => $display_id])];
return $form;
}
$display = &$executable->displayHandlers->get($display_id);
$form['#title'] = $this->t('Rearrange @type', ['@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 = [];
foreach ($display->getHandlers('relationship') as $id => $handler) {
$relationships[$id] = $handler->adminLabel();
}
$form['fields'] = [
'#type' => 'table',
'#header' => ['', $this->t('Weight'), $this->t('Remove')],
'#empty' => $this->t('No fields available.'),
'#tabledrag' => [
[
'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] = [];
$form['fields'][$id]['#attributes'] = ['class' => ['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', ['@id' => $id]);
}
$form['fields'][$id]['name'] = ['#markup' => $markup];
$form['fields'][$id]['weight'] = [
'#type' => 'textfield',
'#default_value' => ++$count,
'#attributes' => ['class' => ['weight']],
'#title' => $this->t('Weight for @title', ['@title' => $name]),
'#title_display' => 'invisible',
];
$form['fields'][$id]['removed'] = [
'#type' => 'checkbox',
'#title' => $this->t('Remove @title', ['@title' => $name]),
'#title_display' => 'invisible',
'#id' => 'views-removed-' . $id,
'#attributes' => ['class' => ['views-remove-checkbox']],
'#default_value' => 0,
'#prefix' => '<div class="js-hide">',
'#suffix' => Markup::create('</div>' . Link::fromTextAndUrl(new FormattableMarkup('<span>@text</span>', ['@text' => $this->t('Remove')]),
Url::fromRoute('<none>', [], [
'attributes' => [
'id' => 'views-remove-link-' . $id,
'class' => ['views-hidden', 'views-button-remove', 'views-remove-link'],
'alt' => $this->t('Remove this item'),
'title' => $this->t('Remove this item'),
],
])
)->toString()),
];
}
$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 = [];
// 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,358 @@
<?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.
*
* @internal
*/
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'] = ['#markup' => $this->t('Invalid display id @display', ['@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', ['@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 = [];
foreach ($display->getHandlers('relationship') as $id => $handler) {
$relationships[$id] = $handler->adminLabel();
}
$group_options = [];
/**
* Filter groups is an array that contains:
* [
* 'operator' => 'and' || 'or',
* 'groups' => [
* $group_id => 'and' || 'or',
* ],
* ];
*/
$grouping = count(array_keys($groups['groups'])) > 1;
$form['filter_groups']['#tree'] = TRUE;
$form['filter_groups']['operator'] = [
'#type' => 'select',
'#options' => [
'AND' => $this->t('And'),
'OR' => $this->t('Or'),
],
'#default_value' => $groups['operator'],
'#attributes' => [
'class' => ['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] = [
'#title' => $this->t('Operator'),
'#type' => 'select',
'#options' => [
'AND' => $this->t('And'),
'OR' => $this->t('Or'),
],
'#default_value' => $group,
'#attributes' => [
'class' => ['warning-on-change'],
],
];
// To prevent a notice.
$form['remove_groups'][$id] = [];
if ($id != 1) {
$form['remove_groups'][$id] = [
'#type' => 'submit',
'#value' => $this->t('Remove group @group', ['@group' => $id]),
'#id' => "views-remove-group-$id",
'#attributes' => [
'class' => ['views-remove-group'],
],
'#group' => $id,
'#ajax' => ['url' => NULL],
];
}
$group_options[$id] = $id == 1 ? $this->t('Default group') : $this->t('Group @group', ['@group' => $id]);
$form['#group_renders'][$id] = [];
}
$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'] = ['#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'] = [
'#title' => $this->t('Weight for @id', ['@id' => $id]),
'#title_display' => 'invisible',
'#type' => 'textfield',
'#default_value' => ++$count,
'#size' => 8,
];
$form['filters'][$id]['group'] = [
'#title' => $this->t('Group for @id', ['@id' => $id]),
'#title_display' => 'invisible',
'#type' => 'select',
'#options' => $group_options,
'#default_value' => $field['group'],
'#attributes' => [
'class' => ['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'] = [
'#markup' => $name,
];
}
else {
$form['filters'][$id]['name'] = ['#markup' => $this->t('Broken field @id', ['@id' => $id])];
}
$form['filters'][$id]['removed'] = [
'#title' => $this->t('Remove @id', ['@id' => $id]),
'#title_display' => 'invisible',
'#type' => 'checkbox',
'#id' => 'views-removed-' . $id,
'#attributes' => ['class' => ['views-remove-checkbox']],
'#default_value' => 0,
];
}
$view->getStandardButtons($form, $form_state, 'views_ui_rearrange_filter_form');
$form['actions']['add_group'] = [
'#type' => 'submit',
'#value' => $this->t('Create new filter group'),
'#id' => 'views-add-group',
'#group' => 'add',
'#attributes' => [
'class' => ['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 = [];
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 = [];
// 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 [0 => 'foo'] would be [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,198 @@
<?php
namespace Drupal\views_ui\Form\Ajax;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
/**
* Displays the display reorder form.
*
* @internal
*/
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 \Drupal\views\ViewEntityInterface $view */
$view = $form_state->get('view');
$display_id = $form_state->get('display_id');
$form['#title'] = $this->t('Reorder displays');
$form['#section'] = 'reorder';
$form['#action'] = Url::fromRoute('views_ui.form_reorder_displays', [
'js' => 'nojs',
'view' => $view->id(),
'display_id' => $display_id,
])->toString();
$form['view'] = [
'#type' => 'value',
'#value' => $view,
];
$displays = $view->get('display');
$count = count($displays);
// Sort the displays.
uasort($displays, function ($display1, $display2) {
return $display1['position'] <=> $display2['position'];
});
$form['displays'] = [
'#type' => 'table',
'#id' => 'reorder-displays',
'#header' => [$this->t('Display'), $this->t('Weight'), $this->t('Remove')],
'#empty' => $this->t('No displays available.'),
'#tabledrag' => [
[
'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] = [
'#display' => $display,
'#attributes' => [
'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'] = [
'#markup' => $display['display_title'],
];
$form['displays'][$id]['weight'] = [
'#type' => 'weight',
'#value' => $display['position'],
'#delta' => $count,
'#title' => $this->t('Weight for @display', ['@display' => $display['display_title']]),
'#title_display' => 'invisible',
'#attributes' => [
'class' => ['weight'],
],
];
$form['displays'][$id]['removed'] = [
'checkbox' => [
'#title' => $this->t('Remove @id', ['@id' => $id]),
'#title_display' => 'invisible',
'#type' => 'checkbox',
'#id' => 'display-removed-' . $id,
'#attributes' => [
'class' => ['views-remove-checkbox'],
],
'#default_value' => !empty($display['deleted']),
],
'link' => [
'#type' => 'link',
'#title' => new FormattableMarkup('<span>@text</span>', ['@text' => $this->t('Remove')]),
'#url' => Url::fromRoute('<none>'),
'#attributes' => [
'id' => 'display-remove-link-' . $id,
'class' => ['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'] = [
'#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 \Drupal\views_ui\ViewUI $view */
$view = $form_state->get('view');
$order = [];
$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->toUrl('edit-form')
->setOption('fragment', 'views-tab-default');
$form_state->setRedirectUrl($url);
}
}

View File

@@ -0,0 +1,283 @@
<?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\Core\Url;
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) {
/** @var \Drupal\Core\Form\FormStateInterface $form_state */
$form_state = $this->getFormState($view, $display_id, $js);
$view = $form_state->get('view');
$form_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([$form_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);
$stack_key = key($view->stack);
$top = current($view->stack);
next($view->stack);
unset($view->stack[$stack_key]);
if (array_shift($top) != $identifier) {
$view->stack = [];
}
}
// 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'] !== $form_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]]);
$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([]);
$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(Url::fromRoute('entity.view.edit_display_form', ['view' => $view->id(), 'display_id' => $display_id], ['absolute' => TRUE])->toString());
}
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::entityTypeManager()->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 = ['#type' => 'status_messages'];
if ($messages = $renderer->renderRoot($status_messages)) {
$display = '<div class="views-messages">' . $messages . '</div>';
}
$display .= $output;
$options = [
'classes' => [
'ui-dialog' => '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() : Url::fromRoute('<current>')->toString();
$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
* A 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,169 @@
<?php
namespace Drupal\views_ui\Form;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\TypedConfigManagerInterface;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\RedundantEditableConfigNamesTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Form builder for the admin display defaults page.
*
* @internal
*/
class BasicSettingsForm extends ConfigFormBase {
use RedundantEditableConfigNamesTrait;
/**
* 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\Config\TypedConfigManagerInterface $typedConfigManager
* The typed config manager.
* @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
* The theme handler.
*/
public function __construct(ConfigFactoryInterface $config_factory, TypedConfigManagerInterface $typedConfigManager, ThemeHandlerInterface $theme_handler) {
parent::__construct($config_factory, $typedConfigManager);
$this->themeHandler = $theme_handler;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('config.factory'),
$container->get('config.typed'),
$container->get('theme_handler')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'views_ui_admin_settings_basic';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form = parent::buildForm($form, $form_state);
$options = [];
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'] = [];
$form['basic']['ui_show_default_display'] = [
'#type' => 'checkbox',
'#title' => $this->t('Always show the default display'),
'#config_target' => 'views.settings:ui.show.default_display',
];
$form['basic']['ui_show_advanced_column'] = [
'#type' => 'checkbox',
'#title' => $this->t('Always show advanced display settings'),
'#config_target' => 'views.settings:ui.show.advanced_column',
];
$form['basic']['ui_show_display_embed'] = [
'#type' => 'checkbox',
'#title' => $this->t('Allow embedded displays'),
'#description' => $this->t('Embedded displays can be used in code via views_embed_view().'),
'#config_target' => 'views.settings:ui.show.display_embed',
];
$form['basic']['ui_exposed_filter_any_label'] = [
'#type' => 'select',
'#title' => $this->t('Label for "Any" value on non-required single-select exposed filters'),
'#options' => ['old_any' => '<Any>', 'new_any' => $this->t('- Any -')],
'#config_target' => 'views.settings:ui.exposed_filter_any_label',
];
$form['live_preview'] = [
'#type' => 'details',
'#title' => $this->t('Live preview settings'),
'#open' => TRUE,
];
$form['live_preview']['ui_always_live_preview'] = [
'#type' => 'checkbox',
'#title' => $this->t('Automatically update preview on changes'),
'#config_target' => 'views.settings:ui.always_live_preview',
];
$form['live_preview']['ui_show_preview_information'] = [
'#type' => 'checkbox',
'#title' => $this->t('Show information and statistics about the view during live preview'),
'#config_target' => 'views.settings:ui.show.preview_information',
];
$form['live_preview']['options'] = [
'#type' => 'container',
'#states' => [
'visible' => [
':input[name="ui_show_preview_information"]' => ['checked' => TRUE],
],
],
];
$form['live_preview']['options']['ui_show_sql_query_enabled'] = [
'#type' => 'checkbox',
'#title' => $this->t('Show the SQL query'),
'#config_target' => 'views.settings:ui.show.sql_query.enabled',
];
$form['live_preview']['options']['ui_show_sql_query_where'] = [
'#type' => 'radios',
'#states' => [
'visible' => [
':input[name="ui_show_sql_query_enabled"]' => ['checked' => TRUE],
],
],
'#title' => $this->t('Show SQL query'),
'#options' => [
'above' => $this->t('Above the preview'),
'below' => $this->t('Below the preview'),
],
'#config_target' => 'views.settings:ui.show.sql_query.where',
];
$form['live_preview']['options']['ui_show_performance_statistics'] = [
'#type' => 'checkbox',
'#title' => $this->t('Show performance statistics'),
'#config_target' => 'views.settings:ui.show.performance_statistics',
];
$form['live_preview']['options']['ui_show_additional_queries'] = [
'#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."),
'#config_target' => 'views.settings:ui.show.additional_queries',
];
return $form;
}
}

View File

@@ -0,0 +1,116 @@
<?php
namespace Drupal\views_ui\Form;
use Drupal\Core\Entity\EntityConfirmFormBase;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\TempStore\SharedTempStoreFactory;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Builds the form to break the lock of an edited view.
*
* @internal
*/
class BreakLockForm extends EntityConfirmFormBase {
/**
* Stores the entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Stores the shared tempstore.
*
* @var \Drupal\Core\TempStore\SharedTempStore
*/
protected $tempStore;
/**
* Constructs a \Drupal\views_ui\Form\BreakLockForm object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\TempStore\SharedTempStoreFactory $temp_store_factory
* The factory for the temp store object.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, SharedTempStoreFactory $temp_store_factory) {
$this->entityTypeManager = $entity_type_manager;
$this->tempStore = $temp_store_factory->get('views');
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager'),
$container->get('tempstore.shared')
);
}
/**
* {@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?', ['%name' => $this->entity->id()]);
}
/**
* {@inheritdoc}
*/
public function getDescription() {
$locked = $this->tempStore->getMetadata($this->entity->id());
$account = $this->entityTypeManager->getStorage('user')->load($locked->getOwnerId());
$username = [
'#theme' => 'username',
'#account' => $account,
];
return $this->t('By breaking this lock, any unsaved changes made by @user will be lost.', ['@user' => \Drupal::service('renderer')->render($username)]);
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return $this->entity->toUrl('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.', ['%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->toUrl('edit-form'));
$this->messenger()->addStatus($this->t('The lock has been broken and you may now edit this view.'));
}
}

View File

@@ -0,0 +1,99 @@
<?php
namespace Drupal\views_ui\ParamConverter;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\ParamConverter\AdminPathConfigEntityConverter;
use Drupal\Core\ParamConverter\ParamConverterInterface;
use Drupal\Core\Routing\AdminContext;
use Drupal\Core\TempStore\SharedTempStoreFactory;
use Drupal\views_ui\ViewUI;
use Symfony\Component\Routing\Route;
/**
* 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\Core\TempStore\SharedTempStoreFactory
*/
protected $tempStoreFactory;
/**
* Constructs a new ViewUIConverter.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\TempStore\SharedTempStoreFactory $temp_store_factory
* The factory for the temp store object.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Drupal\Core\Routing\AdminContext $admin_context
* The route admin context service.
* @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
* The entity repository.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, SharedTempStoreFactory $temp_store_factory, ConfigFactoryInterface $config_factory, AdminContext $admin_context, EntityRepositoryInterface $entity_repository) {
parent::__construct($entity_type_manager, $config_factory, $admin_context, $entity_repository);
$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->setLock($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
// phpcs:ignoreFile
/**
* 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,228 @@
<?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;
use Drupal\Core\Extension\ModuleHandlerInterface;
/**
* Form controller for the Views add form.
*
* @internal
*/
class ViewAddForm extends ViewFormBase {
/**
* The wizard plugin manager.
*
* @var \Drupal\views\Plugin\ViewsPluginManager
*/
protected $wizardManager;
/**
* The module handler service.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* Constructs a new ViewAddForm object.
*
* @param \Drupal\views\Plugin\ViewsPluginManager $wizard_manager
* The wizard plugin manager.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler service.
*/
public function __construct(ViewsPluginManager $wizard_manager, ModuleHandlerInterface $module_handler) {
$this->wizardManager = $wizard_manager;
$this->moduleHandler = $module_handler;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('plugin.manager.views.wizard'),
$container->get('module_handler')
);
}
/**
* {@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'] = ['views-admin'];
$form['name'] = [
'#type' => 'fieldset',
'#title' => $this->t('View basic information'),
'#attributes' => ['class' => ['fieldset-no-legend']],
];
$form['name']['label'] = [
'#type' => 'textfield',
'#title' => $this->t('View name'),
'#required' => TRUE,
'#size' => 32,
'#default_value' => '',
'#maxlength' => 255,
];
$form['name']['id'] = [
'#type' => 'machine_name',
'#maxlength' => 128,
'#machine_name' => [
'exists' => '\Drupal\views\Views::getView',
'source' => ['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'] = [
'#type' => 'checkbox',
'#title' => $this->t('Description'),
];
$form['name']['description'] = [
'#type' => 'textfield',
'#title' => $this->t('Provide description'),
'#title_display' => 'invisible',
'#size' => 64,
'#default_value' => '',
'#states' => [
'visible' => [
':input[name="description_enable"]' => ['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'] = [];
// Create the part of the form that allows the user to select the basic
// properties of what the view will display.
$form['displays']['show'] = [
'#type' => 'fieldset',
'#title' => $this->t('View settings'),
'#tree' => TRUE,
'#attributes' => ['class' => ['container-inline']],
];
// Create the "Show" dropdown, which allows the base table of the view to be
// selected.
$wizard_plugins = $this->wizardManager->getDefinitions();
$options = [];
foreach ($wizard_plugins as $key => $wizard) {
$options[$key] = $wizard['title'];
}
$form['displays']['show']['wizard_key'] = [
'#type' => 'select',
'#title' => $this->t('Show'),
'#options' => $options,
'#sort_options' => TRUE,
];
$show_form = &$form['displays']['show'];
$default_value = $this->moduleHandler->moduleExists('node') ? 'node' : 'users';
$show_form['wizard_key']['#default_value'] = WizardPluginBase::getSelected($form_state, ['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', ['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'] = [[$this, 'submitForm']];
$actions['cancel'] = [
'#type' => 'submit',
'#value' => $this->t('Cancel'),
'#submit' => ['::cancel'],
'#limit_validation_errors' => [],
];
return $actions;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
$wizard_type = $form_state->getValue(['show', 'wizard_key']);
$wizard_instance = $this->wizardManager->createInstance($wizard_type);
$form_state->set('wizard', $wizard_instance->getPluginDefinition());
$form_state->set('wizard_instance', $wizard_instance);
$path = &$form_state->getValue(['page', 'path']);
if (!empty($path)) {
// @todo https://www.drupal.org/node/2423913 Views should expect and store
// a leading /.
$path = ltrim($path, '/ ');
}
$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 \Drupal\views\Plugin\views\wizard\WizardInterface $wizard */
$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) {
$this->messenger()->addError($e->getMessage());
$form_state->setRedirect('entity.view.collection');
return;
}
$this->entity->save();
$this->messenger()->addStatus($this->t('The view %name has been saved.', ['%name' => $form_state->getValue('label')]));
$form_state->setRedirectUrl($this->entity->toUrl('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,146 @@
<?php
namespace Drupal\views_ui;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Form controller for the Views duplicate form.
*
* @internal
*/
class ViewDuplicateForm extends ViewFormBase {
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected LanguageManagerInterface $languageManager;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('module_handler'),
$container->get('language_manager')
);
}
/**
* Constructs a ViewDuplicateForm.
*
* @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler
* Drupal's module handler.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
*/
public function __construct(ModuleHandlerInterface $moduleHandler, LanguageManagerInterface $language_manager) {
$this->setModuleHandler($moduleHandler);
$this->languageManager = $language_manager;
}
/**
* {@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', ['@label' => $this->entity->label()]);
$form['label'] = [
'#type' => 'textfield',
'#title' => $this->t('View name'),
'#required' => TRUE,
'#size' => 32,
'#maxlength' => 255,
'#default_value' => $this->t('Duplicate of @label', ['@label' => $this->entity->label()]),
];
$form['id'] = [
'#type' => 'machine_name',
'#maxlength' => 128,
'#machine_name' => [
'exists' => '\Drupal\views\Views::getView',
'source' => ['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'] = [
'#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) {
// The original ID gets set to NULL when duplicating, so we need to store it
// here.
$original_id = $this->entity->id();
$this->entity = $this->entity->createDuplicate();
$this->entity->set('label', $form_state->getValue('label'));
$this->entity->set('id', $form_state->getValue('id'));
$this->entity->save();
$this->copyTranslations($original_id);
// Redirect the user to the view admin form.
$form_state->setRedirectUrl($this->entity->toUrl('edit-form'));
}
/**
* Copies all translations that existed on the original View.
*
* @param string $original_id
* The original View ID.
*/
private function copyTranslations(string $original_id): void {
if (!$this->moduleHandler->moduleExists('config_translation')) {
return;
}
$current_langcode = $this->languageManager->getConfigOverrideLanguage()
->getId();
$languages = $this->languageManager->getLanguages();
$original_name = 'views.view.' . $original_id;
$duplicate_name = 'views.view.' . $this->entity->id();
foreach ($languages as $language) {
$langcode = $language->getId();
if ($langcode !== $current_langcode) {
$original_translation = $this->languageManager->getLanguageConfigOverride($langcode, $original_name)
->get();
if ($original_translation) {
$duplicate_translation = $this->languageManager->getLanguageConfigOverride($langcode, $duplicate_name);
$duplicate_translation->setData($original_translation);
$duplicate_translation->save();
}
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,174 @@
<?php
namespace Drupal\views_ui;
use Drupal\Component\Utility\Unicode;
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 \Drupal\views_ui\ViewUI $view
* The ViewUI entity.
*
* @return array
* An array of tab definitions.
*/
public function getDisplayTabs(ViewUI $view) {
$executable = $view->getExecutable();
$executable->initDisplay();
$display_id = $this->displayID;
$tabs = [];
// 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] = [
'#theme' => 'menu_local_task',
'#weight' => $display['position'],
'#link' => [
'title' => $this->getDisplayLabel($view, $id),
'localized_options' => [],
'url' => $view->toUrl('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.default_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('Default') : $display[$display_id]['display_title'];
$title = Unicode::truncate($title, 25, FALSE, TRUE);
if ($check_changed && !empty($view->changed_display[$display_id])) {
$changed = '*';
$title = $title . $changed;
}
return $title;
}
}

View File

@@ -0,0 +1,290 @@
<?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\Link;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException;
/**
* 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}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$entity_type,
$container->get('entity_type.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 = [
'enabled' => [],
'disabled' => [],
];
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 [
'data' => [
'view_name' => [
'data' => [
'#plain_text' => $view->label(),
],
],
'machine_name' => [
'data' => [
'#plain_text' => $view->id(),
],
],
'description' => [
'data' => [
'#plain_text' => $view->get('description'),
],
],
'displays' => [
'data' => [
'#theme' => 'views_ui_view_displays_list',
'#displays' => $this->getDisplaysList($view),
],
],
'operations' => $row['operations'],
],
'#attributes' => [
'class' => [$view->status() ? 'views-ui-list-enabled' : 'views-ui-list-disabled'],
],
];
}
/**
* {@inheritdoc}
*/
public function buildHeader() {
return [
'view_name' => [
'data' => $this->t('View name'),
'#attributes' => [
'class' => ['views-ui-name'],
],
],
'machine_name' => [
'data' => $this->t('Machine name'),
'#attributes' => [
'class' => ['views-ui-machine-name'],
],
],
'description' => [
'data' => $this->t('Description'),
'#attributes' => [
'class' => ['views-ui-description'],
],
],
'displays' => [
'data' => $this->t('Displays'),
'#attributes' => [
'class' => ['views-ui-displays'],
],
],
'operations' => [
'data' => $this->t('Operations'),
'#attributes' => [
'class' => ['views-ui-operations'],
],
],
];
}
/**
* {@inheritdoc}
*/
public function getDefaultOperations(EntityInterface $entity) {
$operations = parent::getDefaultOperations($entity);
// Remove destination redirect for Edit operation.
$operations['edit']['url'] = $entity->toUrl('edit-form');
if ($entity->hasLinkTemplate('duplicate-form')) {
$operations['duplicate'] = [
'title' => $this->t('Duplicate'),
'weight' => 15,
'url' => $entity->toUrl('duplicate-form'),
];
}
// Add AJAX functionality to enable/disable operations.
foreach (['enable', 'disable'] as $op) {
if (isset($operations[$op])) {
$operations[$op]['url'] = $entity->toUrl($op);
// Enable and disable operations should use AJAX.
$operations[$op]['attributes']['class'][] = 'use-ajax';
}
}
// ajax.js focuses automatically on the data-drupal-selector element. When
// enabling the view again, focusing on the disable link doesn't work, as it
// is hidden. We assign data-drupal-selector to every link, so it focuses
// on the edit link.
foreach ($operations as &$operation) {
$operation['attributes']['data-drupal-selector'] = 'views-listing-' . $entity->id();
}
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';
$list['filters'] = [
'#type' => 'container',
'#attributes' => [
'class' => ['table-filter', 'js-show'],
],
];
$list['filters']['text'] = [
'#type' => 'search',
'#title' => $this->t('Filter'),
'#title_display' => 'invisible',
'#size' => 60,
'#placeholder' => $this->t('Filter by view name, machine name, description, or display path'),
'#attributes' => [
'class' => ['views-filter-text'],
'data-table' => '.views-listing-table',
'autocomplete' => 'off',
'title' => $this->t('Enter a part of the view name, machine name, description, or display path to filter by.'),
],
];
$list['enabled']['heading']['#markup'] = '<h2>' . $this->t('Enabled', [], ['context' => 'Plural']) . '</h2>';
$list['disabled']['heading']['#markup'] = '<h2>' . $this->t('Disabled', [], ['context' => 'Plural']) . '</h2>';
foreach (['enabled', 'disabled'] as $status) {
$list[$status]['#type'] = 'container';
$list[$status]['#attributes'] = ['class' => ['views-list-section', $status]];
$list[$status]['table'] = [
'#theme' => 'views_ui_views_listing_table',
'#headers' => $this->buildHeader(),
'#attributes' => ['class' => ['views-listing-table', $status]],
];
foreach ($entities[$status] as $entity) {
$list[$status]['table']['#rows'][$entity->id()] = $this->buildRow($entity);
}
}
$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 = [];
$executable = $view->getExecutable();
$executable->initDisplay();
foreach ($executable->displayHandlers as $display) {
$rendered_path = FALSE;
$definition = $display->getPluginDefinition();
if (!empty($definition['admin'])) {
if ($display->hasPath()) {
$path = $display->getPath();
if ($view->status() && !str_contains($path, '%')) {
// Wrap this in a try/catch as trying to generate links to some
// routes may throw a NotAcceptableHttpException if they do not
// respond to HTML, such as RESTExports.
try {
// @todo Views should expect and store a leading /. See:
// https://www.drupal.org/node/2423913
$rendered_path = Link::fromTextAndUrl('/' . $path, Url::fromUserInput('/' . $path))->toString();
}
catch (NotAcceptableHttpException $e) {
$rendered_path = '/' . $path;
}
}
else {
$rendered_path = '/' . $path;
}
}
$displays[] = [
'display' => $definition['admin'],
'path' => $rendered_path,
];
}
}
sort($displays);
return $displays;
}
}

View File

@@ -0,0 +1,109 @@
<?php
namespace Drupal\views_ui;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\WorkspaceSafeFormInterface;
use Drupal\Core\Url;
/**
* Form controller for the Views preview form.
*
* @internal
*/
class ViewPreviewForm extends ViewFormBase implements WorkspaceSafeFormInterface {
/**
* {@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'] = ['class' => ['clearfix']];
$form['controls']['title'] = [
'#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'] = [
'#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'] = [
'#type' => 'textfield',
'#title' => $this->t('Preview with contextual filters:'),
'#description' => $this->t('Separate contextual filter values with a "/". For example, %example.', ['%example' => '40/12/10']),
'#id' => 'preview-args',
];
$args = [];
if ($form_state->getValue('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'] = [
'#weight' => 110,
'#theme_wrappers' => ['container'],
'#attributes' => ['id' => 'views-live-preview', 'class' => ['views-live-preview']],
'preview' => $view->renderPreview($this->displayID, $args),
];
}
$uri = $view->toUrl('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 [
'#attributes' => [
'id' => 'preview-submit-wrapper',
'class' => ['preview-submit-wrapper'],
],
'button' => [
'#type' => 'submit',
'#value' => $this->t('Update preview'),
'#attributes' => ['class' => ['arguments-preview']],
'#submit' => ['::submitPreview'],
'#id' => 'preview-submit',
'#ajax' => [
'url' => Url::fromRoute('entity.view.preview_form', ['view' => $view->id(), 'display_id' => $this->displayID]),
'wrapper' => 'views-preview-wrapper',
'event' => 'click',
'progress' => ['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,24 @@
{#
/**
* @file
* Default theme implementation for views displays on the views listing page.
*
* Available variables:
* - displays: Contains multiple display instances. Each display contains:
* - display: Display name.
* - path: Path to display, if any.
*
* @ingroup themeable
*/
#}
<ul>
{% for display in displays %}
<li>
{% if display.path %}
{{ display.display }} <span data-drupal-selector="views-table-filter-text-source">({{ display.path }})</span>
{% else %}
{{ display.display }}
{% endif %}
</li>
{% endfor %}
</ul>

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,49 @@
{#
/**
* @file
* Default theme implementation for views listing table.
*
* Available variables:
* - headers: Contains table headers.
* - rows: Contains multiple rows. Each row contains:
* - view_name: The human-readable name of the view.
* - machine_name: Machine name of the view.
* - description: The description of the view.
* - displays: List of displays attached to the view.
* - operations: List of available operations.
*
* @see template_preprocess_views_ui_views_listing_table()
*
* @ingroup themeable
*/
#}
<table{{ attributes.addClass('responsive-enabled') }}>
<thead>
<tr>
{% for header in headers %}
<th{{ header.attributes }}>{{ header.data }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for row in rows %}
<tr{{ row.attributes }}>
<td class="views-ui-view-name">
<strong data-drupal-selector="views-table-filter-text-source">{{ row.data.view_name.data }}</strong>
</td>
<td class="views-ui-view-machine-name" data-drupal-selector="views-table-filter-text-source">
{{ row.data.machine_name.data }}
</td>
<td class="views-ui-view-description" data-drupal-selector="views-table-filter-text-source">
{{ row.data.description.data }}
</td>
<td class="views-ui-view-displays">
{{ row.data.displays.data }}
</td>
<td class="views-ui-view-operations">
{{ row.data.operations.data }}
</td>
</tr>
{% endfor %}
</tbody>
</table>

View File

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

View File

@@ -0,0 +1,12 @@
name: 'Views UI Test'
type: module
description: 'Test module for Views UI.'
package: Testing
# version: VERSION
dependencies:
- drupal:views_ui
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

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,12 @@
name: 'Views test field'
type: module
description: 'Add custom global field for testing purposes.'
package: Testing
# version: VERSION
dependencies:
- drupal:views_ui
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,18 @@
<?php
/**
* @file
* ViewsUI Test field module.
*/
use Drupal\Core\Form\FormStateInterface;
/**
* Implements hook_form_FORM_ID_alter() for views_ui_add_handler_form.
*
* Changes the label for one of the tests fields to validate this label is not
* searched on.
*/
function views_ui_test_field_form_views_ui_add_handler_form_alter(&$form, FormStateInterface $form_state) {
$form['options']['name']['#options']['views.views_test_field_1']['title']['data']['#title'] .= ' FIELD_1_LABEL';
}

View File

@@ -0,0 +1,29 @@
<?php
/**
* @file
* Provide views data for testing purposes.
*/
/**
* Implements hook_views_data().
*/
function views_ui_test_field_views_data() {
$data['views']['views_test_field_1'] = [
'title' => t('Views test field 1 - FIELD_1_TITLE'),
'help' => t('Field 1 for testing purposes - FIELD_1_DESCRIPTION'),
'field' => [
'id' => 'views_test_field_1',
],
];
$data['views']['views_test_field_2'] = [
'title' => t('Views test field 2 - FIELD_2_TITLE'),
'help' => t('Field 2 for testing purposes - FIELD_2_DESCRIPTION'),
'field' => [
'id' => 'views_test_field_2',
],
];
return $data;
}

View File

@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\views_ui\Functional;
/**
* Tests the views analyze system.
*
* @group views_ui
*/
class AnalyzeTest extends UITestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['views_ui'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = ['test_view'];
/**
* Tests that analyze works in general.
*/
public function testAnalyzeBasic(): void {
$this->drupalLogin($this->adminUser);
$this->drupalGet('admin/structure/views/view/test_view/edit');
$this->assertSession()->linkExists('Analyze view');
// This redirects the user to the analyze form.
$this->clickLink('Analyze view');
$this->assertSession()->titleEquals('View analysis | Drupal');
foreach (['ok', 'warning', 'error'] as $type) {
// Check that analyze messages with the expected type found.
$this->assertSession()->elementExists('css', 'div.' . $type);
}
// This redirects the user back to the main views edit page.
$this->submitForm([], 'Ok');
}
}

View File

@@ -0,0 +1,115 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\views_ui\Functional;
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}
*/
protected static $modules = ['entity_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
public function testUI(): void {
// Set up a block and an entity_test entity.
$block = Block::create(['id' => 'test_id', 'plugin' => 'system_main_block', 'theme' => 'stark']);
$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->toUrl('edit-form'));
// Add a global NULL argument to the view for testing argument placeholders.
$this->drupalGet("admin/structure/views/nojs/add-handler/{$id}/page_1/argument");
$this->submitForm(['name[views.null]' => TRUE], 'Add and configure contextual filters');
$this->submitForm([], 'Apply');
// Configure both the entity_test area header and the block header to
// reference the given entities.
$this->drupalGet("admin/structure/views/nojs/add-handler/{$id}/page_1/header");
$this->submitForm(['name[views.entity_block]' => TRUE], 'Add and configure header');
$this->submitForm(['options[target]' => $block->id()], 'Apply');
$this->drupalGet("admin/structure/views/nojs/add-handler/{$id}/page_1/header");
$this->submitForm(['name[views.entity_entity_test]' => TRUE], 'Add and configure header');
$this->submitForm(['options[target]' => $entity_test->id()], 'Apply');
$this->submitForm([], 'Save');
// Confirm the correct target identifiers were saved for both entities.
$view = View::load($id);
$header = $view->getDisplay('default')['display_options']['header'];
$this->assertEquals(['entity_block', 'entity_entity_test'], array_keys($header));
$this->assertEquals($block->id(), $header['entity_block']['target']);
$this->assertEquals($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->assertSession()->fieldValueEquals('options[target]', $block->id());
$this->drupalGet("admin/structure/views/nojs/handler/$id/page_1/header/entity_entity_test");
$this->assertSession()->fieldValueEquals('options[target]', $entity_test->id());
// Replace the header target entities with argument placeholders.
$this->drupalGet("admin/structure/views/nojs/handler/{$id}/page_1/header/entity_block");
$this->submitForm(['options[target]' => '{{ raw_arguments.null }}'], 'Apply');
$this->drupalGet("admin/structure/views/nojs/handler/{$id}/page_1/header/entity_entity_test");
$this->submitForm(['options[target]' => '{{ raw_arguments.null }}'], 'Apply');
$this->submitForm([], 'Save');
// Confirm that the argument placeholders are saved.
$view = View::load($id);
$header = $view->getDisplay('default')['display_options']['header'];
$this->assertEquals(['entity_block', 'entity_entity_test'], array_keys($header));
$this->assertEquals('{{ raw_arguments.null }}', $header['entity_block']['target']);
$this->assertEquals('{{ 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->assertSession()->fieldValueEquals('options[target]', '{{ raw_arguments.null }}');
$this->drupalGet("admin/structure/views/nojs/handler/$id/page_1/header/entity_entity_test");
$this->assertSession()->fieldValueEquals('options[target]', '{{ raw_arguments.null }}');
// Change the targets for both headers back to the entities.
$this->drupalGet("admin/structure/views/nojs/handler/{$id}/page_1/header/entity_block");
$this->submitForm(['options[target]' => $block->id()], 'Apply');
$this->drupalGet("admin/structure/views/nojs/handler/{$id}/page_1/header/entity_entity_test");
$this->submitForm(['options[target]' => $entity_test->id()], 'Apply');
$this->submitForm([], '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->assertEquals(['entity_block', 'entity_entity_test'], array_keys($header));
$this->assertEquals($block->id(), $header['entity_block']['target']);
$this->assertEquals($entity_test->uuid(), $header['entity_entity_test']['target']);
}
}

View File

@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\views_ui\Functional;
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 = ['test_argument'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests the 'Specify validation criteria' checkbox functionality.
*/
public function testSpecifyValidation(): void {
// 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->assertEquals('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->assertEquals('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.
*
* @param bool $specify_validation
* The form validation.
*/
protected function saveArgumentHandlerWithValidationOptions($specify_validation) {
$options = [
'options[validate][type]' => 'entity---node',
'options[specify_validation]' => $specify_validation,
];
$this->drupalGet('admin/structure/views/nojs/handler/test_argument/default/argument/id');
$this->submitForm($options, 'Apply');
$this->drupalGet('admin/structure/views/view/test_argument');
$this->submitForm([], 'Save');
}
}

View File

@@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\views_ui\Functional;
/**
* Tests the shared tempstore cache in the UI.
*
* @group views_ui
*/
class CachedDataUITest extends UITestBase {
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = ['test_view'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests the shared tempstore views data in the UI.
*/
public function testCacheData(): void {
$views_admin_user_uid = $this->fullAdminUser->id();
$temp_store = $this->container->get('tempstore.shared')->get('views');
// The view should not be locked.
$this->assertNull($temp_store->getMetadata('test_view'), 'The view is not locked.');
$this->drupalGet('admin/structure/views/view/test_view/edit');
// Make sure we have 'changes' to the view.
$this->drupalGet('admin/structure/views/nojs/display/test_view/default/title');
$this->submitForm([], 'Apply');
$this->assertSession()->pageTextContains('You have unsaved changes.');
$this->assertEquals($views_admin_user_uid, $temp_store->getMetadata('test_view')->getOwnerId(), '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->assertEquals($views_admin_user_uid, $temp_store->getMetadata('test_view')->getOwnerId(), 'The view is locked.');
// Cancel the view edit and make sure the cache is deleted.
$this->submitForm([], 'Cancel');
$this->assertNull($temp_store->getMetadata('test_view'), 'Shared tempstore data has been removed.');
// Test we are redirected to the view listing page.
$this->assertSession()->addressEquals('admin/structure/views');
// Log in with another user and make sure the view is locked and break.
$this->drupalGet('admin/structure/views/nojs/display/test_view/default/title');
$this->submitForm([], 'Apply');
$this->drupalLogin($this->adminUser);
$this->drupalGet('admin/structure/views/view/test_view/edit');
// Test that save and cancel buttons are not shown.
$this->assertSession()->buttonNotExists('Save');
$this->assertSession()->buttonNotExists('Cancel');
// Test we have the break lock link.
$this->assertSession()->linkByHrefExists('admin/structure/views/view/test_view/break-lock');
// Break the lock.
$this->clickLink('break this lock');
$this->submitForm([], 'Break lock');
// Test that save and cancel buttons are shown.
$this->assertSession()->buttonExists('Save');
$this->assertSession()->buttonExists('Cancel');
// Test we can save the view.
$this->drupalGet('admin/structure/views/view/test_view/edit');
$this->submitForm([], 'Save');
$this->assertSession()->pageTextContains("The view Test view has been saved.");
// Test that a deleted view has no tempstore data.
$this->drupalGet('admin/structure/views/nojs/display/test_view/default/title');
$this->submitForm([], 'Apply');
$this->drupalGet('admin/structure/views/view/test_view/delete');
$this->submitForm([], 'Delete');
// No view tempstore data should be returned for this view after deletion.
$this->assertNull($temp_store->getMetadata('test_view'), 'View tempstore data has been removed after deletion.');
}
}

View File

@@ -0,0 +1,190 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\views_ui\Functional;
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 = ['test_view'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* \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(): void {
// 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 = [
'plain' => [
'true' => $custom_true,
'false' => $custom_false,
'test' => 'assertStringContainsString',
],
'allowed tag' => [
'true' => '<p>' . $custom_true . '</p>',
'false' => '<p>' . $custom_false . '</p>',
'test' => 'assertStringContainsString',
],
'disallowed tag' => [
'true' => '<script>' . $custom_true . '</script>',
'false' => '<script>' . $custom_false . '</script>',
'test' => 'assertStringNotContainsString',
],
];
// Run the same tests on each type.
foreach ($custom_values as $type => $values) {
$options = [
'options[type]' => 'custom',
'options[type_custom_true]' => $values['true'],
'options[type_custom_false]' => $values['false'],
];
$this->drupalGet('admin/structure/views/nojs/handler/test_view/default/field/age');
$this->submitForm($options, 'Apply');
// Save the view.
$this->drupalGet('admin/structure/views/view/test_view');
$this->submitForm([], 'Save');
$view = Views::getView('test_view');
$output = $view->preview();
$output = \Drupal::service('renderer')->renderRoot($output);
$this->{$values['test']}($values['true'], (string) $output, "Expected custom boolean TRUE value {$values['true']} in output for $type");
$this->{$values['test']}($values['false'], (string) $output, "Expected custom boolean FALSE value {$values['false']} in output for $type");
}
}
/**
* Tests the setting and output of custom labels for boolean values.
*/
public function testCustomOptionTemplate(): void {
// Install theme to test with template system.
\Drupal::service('theme_installer')->install(['views_test_theme']);
// Set the default theme for Views preview.
$this->config('system.theme')
->set('default', 'views_test_theme')
->save();
$this->assertEquals('views_test_theme', $this->config('system.theme')->get('default'));
// 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 = [
'plain' => [
'true' => $custom_true,
'false' => $custom_false,
'test' => 'assertStringContainsString',
],
'allowed tag' => [
'true' => '<p>' . $custom_true . '</p>',
'false' => '<p>' . $custom_false . '</p>',
'test' => 'assertStringContainsString',
],
'disallowed tag' => [
'true' => '<script>' . $custom_true . '</script>',
'false' => '<script>' . $custom_false . '</script>',
'test' => 'assertStringNotContainsString',
],
];
// Run the same tests on each type.
foreach ($custom_values as $type => $values) {
$options = [
'options[type]' => 'custom',
'options[type_custom_true]' => $values['true'],
'options[type_custom_false]' => $values['false'],
];
$this->drupalGet('admin/structure/views/nojs/handler/test_view/default/field/age');
$this->submitForm($options, 'Apply');
// Save the view.
$this->drupalGet('admin/structure/views/view/test_view');
$this->submitForm([], 'Save');
$view = Views::getView('test_view');
$output = $view->preview();
$output = \Drupal::service('renderer')->renderRoot($output);
$this->{$values['test']}($values['true'], (string) $output, "Expected custom boolean TRUE value {$values['true']} in output for $type");
$this->{$values['test']}($values['false'], (string) $output, "Expected custom boolean FALSE value {$values['false']} in output for $type");
// Assert that we are using the correct template.
$this->assertStringContainsString('llama', (string) $output);
}
}
}

View File

@@ -0,0 +1,226 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\views_ui\Functional;
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 = ['test_view_status', 'test_page_display_menu', 'test_page_display_arguments'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp($import_test_views = TRUE, $modules = ['views_test_config']): void {
parent::setUp($import_test_views, $modules);
$this->placeBlock('page_title_block');
}
/**
* Tests default views.
*/
public function testDefaultViews(): void {
// 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->assertSession()->linkByHrefNotExists($edit_href);
// Enable the view, and make sure it is now visible on the main listing
// page.
$this->drupalGet('admin/structure/views');
$this->clickViewsOperationLink('Enable', '/glossary/');
$this->assertSession()->addressEquals('admin/structure/views');
$this->assertSession()->linkByHrefExists($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->assertSession()->linkNotExists('Revert');
// $revert_href = 'admin/structure/views/view/glossary/revert';
// $this->assertSession()->linkByHrefNotExists($revert_href);
// Edit the view and change the title. Make sure that the new title is
// displayed.
$new_title = $this->randomMachineName(16);
$edit = ['title' => $new_title];
$this->drupalGet('admin/structure/views/nojs/display/glossary/page_1/title');
$this->submitForm($edit, 'Apply');
$this->drupalGet('admin/structure/views/view/glossary/edit/page_1');
$this->submitForm([], 'Save');
$this->drupalGet('glossary');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains($new_title);
// Save another view in the UI.
$this->drupalGet('admin/structure/views/nojs/display/archive/page_1/title');
$this->submitForm([], 'Apply');
$this->drupalGet('admin/structure/views/view/archive/edit/page_1');
$this->submitForm([], 'Save');
// Check there is an enable link. i.e. The view has not been enabled after
// editing.
$this->drupalGet('admin/structure/views');
$this->assertSession()->linkByHrefExists('admin/structure/views/view/archive/enable');
// Enable it again so it can be tested for access permissions.
$this->clickViewsOperationLink('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->assertSession()->linkExists('Revert');
// $this->assertSession()->linkByHrefExists($revert_href);
// $this->drupalGet($revert_href);
// $this->submitForm(array(), 'Revert');
// $this->drupalGet('glossary');
// $this->assertSession()->pageTextNotContains($new_title);
// Duplicate the view and check that the normal schema of duplicated views is used.
$this->drupalGet('admin/structure/views');
$this->clickViewsOperationLink('Duplicate', '/glossary');
$edit = [
'id' => 'duplicate_of_glossary',
];
$this->assertSession()->titleEquals('Duplicate of Glossary | Drupal');
$this->submitForm($edit, 'Duplicate');
$this->assertSession()->addressEquals('admin/structure/views/view/duplicate_of_glossary');
// Duplicate a view and set a custom name.
$this->drupalGet('admin/structure/views');
$this->clickViewsOperationLink('Duplicate', '/glossary');
$random_name = $this->randomMachineName();
$this->submitForm(['id' => $random_name], 'Duplicate');
$this->assertSession()->addressEquals("admin/structure/views/view/$random_name");
// 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('Disable', '/glossary/');
// $this->assertSession()->addressEquals('admin/structure/views');
// $this->assertSession()->linkByHrefNotExists($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('Enable', '/glossary/');
$this->assertSession()->addressEquals('admin/structure/views');
$this->assertSession()->linkByHrefExists($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->assertSession()->statusCodeEquals(403);
$this->drupalGet('archive');
$this->assertSession()->statusCodeEquals(403);
// Test deleting a view.
$this->drupalLogin($this->fullAdminUser);
$this->drupalGet('admin/structure/views');
$this->clickViewsOperationLink('Delete', '/glossary/');
// Submit the confirmation form.
$this->submitForm([], 'Delete');
// Ensure the view is no longer listed.
$this->assertSession()->addressEquals('admin/structure/views');
$this->assertSession()->linkByHrefNotExists($edit_href);
// Ensure the view is no longer available.
$this->drupalGet($edit_href);
$this->assertSession()->statusCodeEquals(404);
$this->assertSession()->pageTextContains('Page not found');
// Delete all duplicated Glossary views.
$this->drupalGet('admin/structure/views');
$this->clickViewsOperationLink('Delete', 'duplicate_of_glossary');
// Submit the confirmation form.
$this->submitForm([], 'Delete');
$this->drupalGet('glossary');
$this->assertSession()->statusCodeEquals(200);
$this->drupalGet('admin/structure/views');
$this->clickViewsOperationLink('Delete', $random_name);
// Submit the confirmation form.
$this->submitForm([], 'Delete');
$this->drupalGet('glossary');
$this->assertSession()->statusCodeEquals(404);
$this->assertSession()->pageTextContains('Page not found');
}
/**
* Tests that enabling views moves them to the correct table.
*/
public function testSplitListing(): void {
$this->drupalGet('admin/structure/views');
$this->assertSession()->elementNotExists('xpath', '//div[@id="views-entity-list"]/div[@class = "views-list-section enabled"]/table//td/text()[contains(., "test_view_status")]');
$this->assertSession()->elementsCount('xpath', '//div[@id="views-entity-list"]/div[@class = "views-list-section disabled"]/table//td/text()[contains(., "test_view_status")]', 1);
// Enable the view.
$this->clickViewsOperationLink('Enable', '/test_view_status/');
$this->assertSession()->elementNotExists('xpath', '//div[@id="views-entity-list"]/div[@class = "views-list-section disabled"]/table//td/text()[contains(., "test_view_status")]');
$this->assertSession()->elementsCount('xpath', '//div[@id="views-entity-list"]/div[@class = "views-list-section enabled"]/table//td/text()[contains(., "test_view_status")]', 1);
// Attempt to disable the view by path directly, with no token.
$this->drupalGet('admin/structure/views/view/test_view_status/disable');
$this->assertSession()->statusCodeEquals(403);
}
/**
* Tests that page displays show the correct path.
*/
public function testPathDestination(): void {
$this->drupalGet('admin/structure/views');
// Check that links to views on default tabs are rendered correctly.
$this->assertSession()->linkByHrefExists('test_page_display_menu');
$this->assertSession()->linkByHrefNotExists('test_page_display_menu/default');
$this->assertSession()->linkByHrefExists('test_page_display_menu/local');
// Check that a dynamic path is shown as text.
$this->assertSession()->responseContains('test_route_with_suffix/%/suffix');
$this->assertSession()->linkByHrefNotExists(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.
*/
public function clickViewsOperationLink($label, $unique_href_part) {
$this->assertSession()->elementExists('xpath', "//a[normalize-space(text())='$label' and contains(@href, '$unique_href_part')]")->click();
}
}

View File

@@ -0,0 +1,139 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\views_ui\Functional;
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 = ['test_attachment_ui'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests the attachment UI.
*/
public function testAttachmentUI(): void {
$this->drupalGet('admin/structure/views/view/test_attachment_ui/edit/attachment_1');
$this->assertSession()->pageTextContains('Not defined');
$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->assertSession()->assertEscaped('<em>Page</em>');
$this->assertSession()->checkboxNotChecked("edit-displays-default");
$this->assertSession()->checkboxNotChecked("edit-displays-page-1");
// Save the attachments and test the value on the view.
$this->drupalGet($attachment_display_url);
$this->submitForm(['displays[page_1]' => 1], 'Apply');
// Options summary should be escaped.
$this->assertSession()->assertEscaped('<em>Page</em>');
$this->assertSession()->responseNotContains('<em>Page</em>');
$this->assertSession()->elementAttributeContains('xpath', '//a[@id = "views-attachment-1-displays"]', 'title', 'Page');
$this->submitForm([], 'Save');
$view = Views::getView('test_attachment_ui');
$view->initDisplay();
$this->assertEquals(['page_1'], array_keys(array_filter($view->displayHandlers->get('attachment_1')->getOption('displays'))), 'The attached displays got saved as expected');
$this->drupalGet($attachment_display_url);
$this->submitForm([
'displays[default]' => 1,
'displays[page_1]' => 1,
], 'Apply');
$this->assertSession()->elementAttributeContains('xpath', '//a[@id = "views-attachment-1-displays"]', 'title', 'Multiple displays');
$this->submitForm([], 'Save');
$view = Views::getView('test_attachment_ui');
$view->initDisplay();
$this->assertEquals(['default', 'page_1'], array_keys($view->displayHandlers->get('attachment_1')->getOption('displays')), 'The attached displays got saved as expected');
}
/**
* Tests the attachment working after the attached page was deleted.
*/
public function testRemoveAttachedDisplay(): void {
// Create a view.
$view = $this->randomView();
$path_prefix = 'admin/structure/views/view/' . $view['id'] . '/edit';
$attachment_display_url = 'admin/structure/views/nojs/display/' . $view['id'] . '/attachment_1/displays';
// Open the Page display and create the attachment display.
$this->drupalGet($path_prefix . '/page_1');
$this->submitForm([], 'Add Attachment');
$this->assertSession()->pageTextContains('Not defined');
// Attach the Attachment to the Page display.
$this->drupalGet($attachment_display_url);
$this->submitForm(['displays[page_1]' => 1], 'Apply');
$this->submitForm([], 'Save');
// Open the Page display and mark it as deleted.
$this->drupalGet($path_prefix . '/page_1');
$this->assertSession()->buttonExists('edit-displays-settings-settings-content-tab-content-details-top-actions-delete');
$this->drupalGet($path_prefix . '/page_1');
$this->submitForm([], 'Delete Page');
// Open the attachment display and save it.
$this->drupalGet($path_prefix . '/attachment_1');
$this->submitForm([], 'Save');
// Check that there is no warning for the removed page display.
$this->assertSession()->pageTextNotContains("Plugin ID 'page_1' was not found.");
// Check that the attachment is no longer linked to the removed display.
$this->assertSession()->pageTextContains('Not defined');
}
/**
* Tests the attachment after changing machine name.
*/
public function testAttachmentOnAttachedMachineNameChange(): void {
$view = $this->randomView();
$path_prefix = 'admin/structure/views/view/' . $view['id'] . '/edit';
$attachment_display_url = 'admin/structure/views/nojs/display/' . $view['id'] . '/attachment_1/displays';
// Open the Page display and create the attachment display.
$this->drupalGet($path_prefix . '/page_1');
$this->submitForm([], 'Add Attachment');
$this->assertSession()->pageTextContains('Not defined');
// Attach the Attachment to the Default and Page display.
$this->drupalGet($attachment_display_url);
$this->submitForm(['displays[default]' => 1, 'displays[page_1]' => 1], 'Apply');
$this->submitForm([], 'Save');
// Change the machine name of the page.
$this->drupalGet('admin/structure/views/nojs/display/' . $view['id'] . '/page_1/display_id');
$this->submitForm(['display_id' => 'page_new_id'], 'Apply');
$this->submitForm([], 'Save');
// Check that the attachment is still attached to the page.
$this->drupalGet($attachment_display_url);
$this->assertSession()->checkboxChecked("edit-displays-default");
$this->assertSession()->checkboxChecked("edit-displays-page-new-id");
}
}

View File

@@ -0,0 +1,174 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\views_ui\Functional;
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 = ['test_display'];
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['contextual'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests adding a display.
*/
public function testAddDisplay(): void {
// Show the default display.
$this->config('views.settings')->set('ui.show.default_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->submitForm([], 'Add Page');
$this->assertSession()->linkByHrefExists($path_prefix . '/page_1', 0, 'Make sure after adding a display the new display appears in the UI');
$this->assertSession()->linkNotExists('Default*', 'Make sure the default display is not marked as changed.');
$this->assertSession()->linkExists('Page*', 0, 'Make sure the added display is marked as changed.');
$this->drupalGet("admin/structure/views/nojs/display/{$view['id']}/page_1/path");
$this->submitForm(['path' => 'test/path'], 'Apply');
$this->submitForm([], 'Save');
}
/**
* Tests removing a display.
*/
public function testRemoveDisplay(): void {
$view = $this->randomView();
$path_prefix = 'admin/structure/views/view/' . $view['id'] . '/edit';
// Make sure there is no delete button on the default display.
$this->drupalGet($path_prefix . '/default');
$this->assertSession()->buttonNotExists('edit-displays-settings-settings-content-tab-content-details-top-actions-delete');
$this->drupalGet($path_prefix . '/page_1');
$this->assertSession()->buttonExists('edit-displays-settings-settings-content-tab-content-details-top-actions-delete');
// Delete the page, so we can test the undo process.
$this->drupalGet($path_prefix . '/page_1');
$this->submitForm([], 'Delete Page');
$this->assertSession()->buttonExists('edit-displays-settings-settings-content-tab-content-details-top-actions-undo-delete');
// Test that the display link is marked as to be deleted.
$this->assertSession()->elementExists('xpath', "//a[contains(@href, '{$path_prefix}/page_1') and contains(@class, 'views-display-deleted-link')]");
// Undo the deleting of the display.
$this->drupalGet($path_prefix . '/page_1');
$this->submitForm([], 'Undo delete of Page');
$this->assertSession()->buttonNotExists('edit-displays-settings-settings-content-tab-content-details-top-actions-undo-delete');
$this->assertSession()->buttonExists('edit-displays-settings-settings-content-tab-content-details-top-actions-delete');
// Now delete again and save the view.
$this->drupalGet($path_prefix . '/page_1');
$this->submitForm([], 'Delete Page');
$this->submitForm([], 'Save');
$this->assertSession()->linkByHrefNotExists($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->drupalGet("admin/structure/views/nojs/display/{$view['id']}/page_1/display_id");
$this->submitForm(['display_id' => $machine_name], 'Apply');
$this->submitForm([], 'Delete Page');
$this->submitForm([], 'Save');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->linkByHrefNotExists($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(): void {
$this->drupalGet('admin/structure/views/view/test_display');
$this->assertSession()->elementsCount('xpath', '//*[@id="views-page-1-display-title"]', 1);
}
/**
* Tests the duplicating of a display.
*/
public function testDuplicateDisplay(): void {
$view = $this->randomView();
$path_prefix = 'admin/structure/views/view/' . $view['id'] . '/edit';
$path = $view['page[path]'];
$this->drupalGet($path_prefix);
$this->submitForm([], 'Duplicate Page');
// Verify that the user got redirected to the new display.
$this->assertSession()->linkByHrefExists($path_prefix . '/page_2', 0, 'Make sure after duplicating the new display appears in the UI');
$this->assertSession()->addressEquals($path_prefix . '/page_2');
// Set the title and override the css classes.
$random_title = $this->randomMachineName();
$random_css = $this->randomMachineName();
$this->drupalGet("admin/structure/views/nojs/display/{$view['id']}/page_2/title");
$this->submitForm(['title' => $random_title], 'Apply');
$this->drupalGet("admin/structure/views/nojs/display/{$view['id']}/page_2/css_class");
$this->submitForm([
'override[dropdown]' => 'page_2',
'css_class' => $random_css,
], 'Apply');
// Duplicate as a different display type.
$this->submitForm([], 'Duplicate as Block');
$this->assertSession()->linkByHrefExists($path_prefix . '/block_1', 0, 'Make sure after duplicating the new display appears in the UI');
$this->assertSession()->addressEquals($path_prefix . '/block_1');
$this->assertSession()->pageTextContains('Block settings');
$this->assertSession()->pageTextNotContains('Page settings');
$this->submitForm([], 'Save');
$view = Views::getView($view['id']);
$view->initDisplay();
$page_2 = $view->displayHandlers->get('page_2');
$this->assertNotEmpty($page_2, 'The new page display got saved.');
$this->assertEquals('Page', $page_2->display['display_title']);
$this->assertEquals($path, $page_2->display['display_options']['path']);
$block_1 = $view->displayHandlers->get('block_1');
$this->assertNotEmpty($block_1, 'The new block display got saved.');
$this->assertEquals('block', $block_1->display['display_plugin']);
$this->assertEquals('Block', $block_1->display['display_title'], 'The new display title got generated as expected.');
$this->assertFalse(isset($block_1->display['display_options']['path']));
$this->assertEquals($random_title, $block_1->getOption('title'), 'The overridden title option from the display got copied into the duplicate');
$this->assertEquals($random_css, $block_1->getOption('css_class'), 'The overridden css_class option from the display got copied into the duplicate');
// Test duplicating a display after changing the machine name.
$view_id = $view->id();
$this->drupalGet("admin/structure/views/nojs/display/{$view_id}/page_2/display_id");
$this->submitForm(['display_id' => 'page_new'], 'Apply');
$this->submitForm([], 'Duplicate as Block');
$this->submitForm([], 'Save');
$view = Views::getView($view_id);
$view->initDisplay();
$this->assertNotNull($view->displayHandlers->get('page_new'), 'The original display is saved with a changed id');
$this->assertNotNull($view->displayHandlers->get('block_2'), 'The duplicate display is saved with new id');
}
}

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\views_ui\Functional;
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 = ['test_view'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests the display extender UI.
*/
public function testDisplayExtenderUI(): void {
$this->config('views.settings')->set('display_extenders', ['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->assertSession()->linkByHrefExists($display_option_url, 0, 'Make sure the option defined by the test display extender appears in the UI.');
$random_text = $this->randomMachineName();
$this->drupalGet($display_option_url);
$this->submitForm(['test_extender_test_option' => $random_text], 'Apply');
$this->assertSession()->linkExists($random_text);
$this->submitForm([], 'Save');
$view = Views::getView($view->storage->id());
$view->initDisplay();
$display_extender_options = $view->display_handler->getOption('display_extenders');
$this->assertEquals($random_text, $display_extender_options['display_extender_test']['test_extender_test_option'], 'Make sure that the display extender option got saved.');
}
}

View File

@@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\views_ui\Functional;
/**
* 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 = ['test_display_feed'];
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['views_ui'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests feed display admin UI.
*/
public function testFeedUI(): void {
// Test the RSS feed.
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->assertSession()->pageTextContains($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->assertSession()->assertEscaped('<em>Page</em>');
// Load all the options of the checkbox.
$result = $this->xpath('//div[@id="edit-displays"]/div');
$options = [];
foreach ($result as $item) {
$input_node = $item->find('css', 'input');
if ($input_node->hasAttribute('value')) {
$options[] = $input_node->getAttribute('value');
}
}
$this->assertEquals(['default', 'page'], $options, 'Make sure all displays appears as expected.');
// Post and save this and check the output.
$this->drupalGet('admin/structure/views/nojs/display/' . $view_name . '/feed_1/displays');
$this->submitForm(['displays[page]' => 'page'], 'Apply');
// Options summary should be escaped.
$this->assertSession()->assertEscaped('<em>Page</em>');
$this->assertSession()->responseNotContains('<em>Page</em>');
$this->drupalGet('admin/structure/views/view/' . $view_name . '/edit/feed_1');
$this->assertSession()->elementTextContains('xpath', '//*[@id="views-feed-1-displays"]', 'Page');
// Add the default display, so there should now be multiple displays.
$this->drupalGet('admin/structure/views/nojs/display/' . $view_name . '/feed_1/displays');
$this->submitForm(['displays[default]' => 'default'], 'Apply');
$this->drupalGet('admin/structure/views/view/' . $view_name . '/edit/feed_1');
$this->assertSession()->elementTextContains('xpath', '//*[@id="views-feed-1-displays"]', 'Multiple displays');
}
}

View File

@@ -0,0 +1,347 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\views_ui\Functional;
use Drupal\Core\Menu\MenuTreeParameters;
use Drupal\menu_link_content\Entity\MenuLinkContent;
use Drupal\Tests\SchemaCheckTestTrait;
use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait;
/**
* Tests the UI of generic display path plugin.
*
* @group views_ui
* @group #slow
* @see \Drupal\views\Plugin\views\display\PathPluginBase
*/
class DisplayPathTest extends UITestBase {
use AssertPageCacheContextsAndTagsTrait;
use SchemaCheckTestTrait;
/**
* {@inheritdoc}
*/
protected function setUp($import_test_views = TRUE, $modules = ['views_test_config']): void {
parent::setUp($import_test_views, $modules);
$this->placeBlock('page_title_block');
}
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = ['test_view', 'test_page_display_menu'];
/**
* Runs the tests.
*/
public function testPathUI(): void {
$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->submitForm([], 'Add Page');
$this->assertSession()->pageTextContains('No path is set');
$this->assertSession()->linkNotExists('View 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->drupalGet('admin/structure/views/nojs/display/test_view/page_1/path');
$this->submitForm(['path' => $random_path], 'Apply');
$this->assertSession()->pageTextContains('/' . $random_path);
$this->clickLink('View Page');
$this->assertSession()->addressEquals($random_path);
}
/**
* Tests that View paths are properly filtered for XSS.
*/
public function doPathXssFilterTest() {
$this->drupalGet('admin/structure/views/view/test_view');
$this->submitForm([], 'Add Page');
$this->drupalGet('admin/structure/views/nojs/display/test_view/page_2/path');
$this->submitForm(['path' => '<object>malformed_path</object>'], 'Apply');
$this->submitForm([], 'Add Page');
$this->drupalGet('admin/structure/views/nojs/display/test_view/page_3/path');
$this->submitForm(['path' => '<script>alert("hello");</script>'], 'Apply');
$this->submitForm([], 'Add Page');
$this->drupalGet('admin/structure/views/nojs/display/test_view/page_4/path');
$this->submitForm(['path' => '<script>alert("hello I have placeholders %");</script>'], 'Apply');
$this->drupalGet('admin/structure/views/view/test_view');
$this->submitForm([], 'Save');
$this->drupalGet('admin/structure/views');
// The anchor text should be escaped.
$this->assertSession()->assertEscaped('/<object>malformed_path</object>');
$this->assertSession()->assertEscaped('/<script>alert("hello");</script>');
$this->assertSession()->assertEscaped('/<script>alert("hello I have placeholders %");</script>');
// Links should be URL-encoded.
$this->assertSession()->responseContains('/%3Cobject%3Emalformed_path%3C/object%3E');
$this->assertSession()->responseContains('/%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->drupalGet($url);
$this->submitForm(['path' => '%/foo'], 'Apply');
$this->assertSession()->addressEquals($url);
$this->assertSession()->pageTextContains('"%" may not be used for the first segment of a path.');
$this->drupalGet($url);
$this->submitForm(['path' => 'user/%1/example'], 'Apply');
$this->assertSession()->addressEquals($url);
$this->assertSession()->pageTextContains("Numeric placeholders may not be used. Use plain placeholders (%).");
}
/**
* Tests deleting a page display that has no path.
*/
public function testDeleteWithNoPath(): void {
$this->drupalGet('admin/structure/views/view/test_view');
$this->submitForm([], 'Add Page');
$this->submitForm([], 'Delete Page');
$this->submitForm([], 'Save');
$this->assertSession()->pageTextContains("The view Test view has been saved.");
}
/**
* Tests the menu and tab option form.
*/
public function testMenuOptions(): void {
$this->drupalGet('admin/structure/views/view/test_view');
// Add a new page display.
$this->submitForm([], 'Add Page');
// Add an invalid path (only fragment).
$this->drupalGet('admin/structure/views/nojs/display/test_view/page_1/path');
$this->submitForm(['path' => '#foo'], 'Apply');
$this->assertSession()->pageTextContains('Path is empty');
// Add an invalid path with a query.
$this->drupalGet('admin/structure/views/nojs/display/test_view/page_1/path');
$this->submitForm(['path' => 'foo?bar'], 'Apply');
$this->assertSession()->pageTextContains('No query allowed.');
// Add an invalid path with just a query.
$this->drupalGet('admin/structure/views/nojs/display/test_view/page_1/path');
$this->submitForm(['path' => '?bar'], 'Apply');
$this->assertSession()->pageTextContains('Path is empty');
// Provide a random, valid path string.
$random_string = $this->randomMachineName();
// Save a path.
$this->drupalGet('admin/structure/views/nojs/display/test_view/page_1/path');
$this->submitForm(['path' => $random_string], 'Apply');
$this->drupalGet('admin/structure/views/view/test_view');
$this->drupalGet('admin/structure/views/nojs/display/test_view/page_1/menu');
$this->submitForm([
'menu[type]' => 'default tab',
'menu[title]' => 'Test tab title',
], 'Apply');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->addressEquals('admin/structure/views/nojs/display/test_view/page_1/tab_options');
$this->submitForm(['tab_options[type]' => 'tab', 'tab_options[title]' => $this->randomString()], 'Apply');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->addressEquals('admin/structure/views/view/test_view/edit/page_1');
$this->drupalGet('admin/structure/views/view/test_view');
$this->assertSession()->linkExists('Tab: Test tab title');
// If it's a default tab, it should also have an additional settings link.
$this->assertSession()->linkByHrefExists('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->assertSession()->statusCodeEquals(200);
$menu_options = $this->assertSession()->selectExists('edit-menu-parent')->findAll('css', 'option');
$menu_options = array_map(function ($element) {
return $element->getText();
}, $menu_options);
$this->assertEquals([
'<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(): void {
$this->container->get('module_installer')->install(['menu_link_content', 'toolbar', 'system']);
$this->resetAll();
$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',
'access administration pages',
]);
$this->drupalLogin($admin_user);
$edit = [
'title[0][value]' => 'Menu title',
'link[0][uri]' => '/admin/foo',
'menu_parent' => 'admin:system.admin',
];
$this->drupalGet('admin/structure/menu/manage/admin/add');
$this->submitForm($edit, 'Save');
$menu_items = \Drupal::entityTypeManager()->getStorage('menu_link_content')->getQuery()
->accessCheck(FALSE)
->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'] = $this->randomMachineName(16);
$edit['description'] = $this->randomMachineName(16);
$edit['page[create]'] = TRUE;
$edit['page[path]'] = 'admin/foo';
$this->drupalGet('admin/structure/views/add');
$this->submitForm($edit, '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->assertEquals('view.' . $view_id . '.page_1', $plugin_definition['route_name']);
$this->clickLink('No menu');
$this->submitForm([
'menu[type]' => 'default tab',
'menu[title]' => 'Menu title',
], 'Apply');
$this->assertSession()->pageTextContains('Default tab options');
$this->submitForm([
'tab_options[type]' => 'normal',
'tab_options[title]' => 'Parent title',
], 'Apply');
// Open the menu options again.
$this->clickLink('Tab: Menu title');
// Assert a menu can be selected as a parent.
$this->assertSession()->optionExists('menu[parent]', 'admin:');
// Assert a parent menu item can be selected from within a menu.
$this->assertSession()->optionExists('menu[parent]', 'admin:system.admin');
// Check that parent menu item can now be
// added without the menu_ui module being enabled.
$this->submitForm([
'menu[type]' => 'normal',
'menu[parent]' => 'admin:system.admin',
'menu[title]' => 'Menu title',
], 'Apply');
$this->submitForm([], 'Save');
// Assert that saving the view will not cause an exception.
$this->assertSession()->statusCodeEquals(200);
}
/**
* Tests the "Use the administration theme" configuration.
*
* @see \Drupal\Tests\views\Functional\Plugin\DisplayPageWebTest::testAdminTheme
*/
public function testUseAdminTheme(): void {
$this->drupalGet('admin/structure/views/view/test_view');
// Add a new page display.
$this->submitForm([], 'Add Page');
$this->assertSession()->pageTextContains('No path is set');
$this->assertSession()->pageTextContains('Administration theme: No');
// Test with a path starting with "/admin".
$admin_path = 'admin/test_admin_path';
$this->drupalGet('admin/structure/views/nojs/display/test_view/page_1/path');
$this->submitForm(['path' => $admin_path], 'Apply');
$this->assertSession()->pageTextContains('/' . $admin_path);
$this->assertSession()->pageTextContains('Administration theme: Yes (admin path)');
$this->submitForm([], 'Save');
$this->assertConfigSchemaByName('views.view.test_view');
$display_options = $this->config('views.view.test_view')->get('display.page_1.display_options');
$this->assertArrayNotHasKey('use_admin_theme', $display_options);
$this->drupalGet('admin/structure/views/nojs/display/test_view/page_1/use_admin_theme');
$this->assertSession()->elementExists('css', 'input[name="use_admin_theme"][disabled="disabled"][checked="checked"]');
// Test with a non-administration path.
$non_admin_path = 'kittens';
$this->drupalGet('admin/structure/views/nojs/display/test_view/page_1/path');
$this->submitForm(['path' => $non_admin_path], 'Apply');
$this->assertSession()->pageTextContains('/' . $non_admin_path);
$this->assertSession()->pageTextContains('Administration theme: No');
$this->submitForm([], 'Save');
$this->assertConfigSchemaByName('views.view.test_view');
$display_options = $this->config('views.view.test_view')->get('display.page_1.display_options');
$this->assertArrayNotHasKey('use_admin_theme', $display_options);
// Enable administration theme.
$this->drupalGet('admin/structure/views/nojs/display/test_view/page_1/use_admin_theme');
$this->submitForm(['use_admin_theme' => TRUE], 'Apply');
$this->assertSession()->pageTextContains('Administration theme: Yes');
$this->submitForm([], 'Save');
$this->assertConfigSchemaByName('views.view.test_view');
$display_options = $this->config('views.view.test_view')->get('display.page_1.display_options');
$this->assertArrayHasKey('use_admin_theme', $display_options);
$this->assertTrue($display_options['use_admin_theme']);
}
}

View File

@@ -0,0 +1,308 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\views_ui\Functional;
use Drupal\Component\Utility\Unicode;
use Drupal\views\Entity\View;
use Drupal\views\Views;
/**
* Tests the display UI.
*
* @group views_ui
* @group #slow
*/
class DisplayTest extends UITestBase {
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = ['test_display'];
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['contextual'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests adding a display.
*/
public function testAddDisplay(): void {
$view = $this->randomView();
$this->assertSession()->elementNotExists('xpath', '//li[@data-drupal-selector="edit-displays-top-tabs-block-1"]');
$this->assertSession()->elementNotExists('xpath', '//li[@data-drupal-selector="edit-displays-top-tabs-block-2"]');
$this->assertSession()->pageTextMatchesCount(0, '/Block name:/');
$this->submitForm([], 'Add Block');
$this->assertSession()->elementTextContains('xpath', '//li[@data-drupal-selector="edit-displays-top-tabs-block-1"]', 'Block*');
$this->assertSession()->elementNotExists('xpath', '//li[@data-drupal-selector="edit-displays-top-tabs-block-2"]');
$this->assertSession()->pageTextMatchesCount(1, '/Block name:/');
}
/**
* Tests reordering of displays.
*/
public function testReorderDisplay(): void {
$view = [
'block[create]' => TRUE,
];
$view = $this->randomView($view);
$this->clickLink('Reorder displays');
$this->assertSession()->elementExists('xpath', '//tr[@id="display-row-default"]');
$this->assertSession()->elementExists('xpath', '//tr[@id="display-row-page_1"]');
$this->assertSession()->elementExists('xpath', '//tr[@id="display-row-block_1"]');
// Ensure the view displays are in the expected order in configuration.
$expected_display_order = ['default', 'block_1', 'page_1'];
$this->assertEquals($expected_display_order, array_keys(Views::getView($view['id'])->storage->get('display')), 'The correct display names are present.');
// Put the block display in front of the page display.
$edit = [
'displays[page_1][weight]' => 2,
'displays[block_1][weight]' => 1,
];
$this->submitForm($edit, 'Apply');
$this->submitForm([], 'Save');
$view = Views::getView($view['id']);
$displays = $view->storage->get('display');
$this->assertEquals(0, $displays['default']['position'], 'Make sure the default display comes first.');
$this->assertEquals(1, $displays['block_1']['position'], 'Make sure the block display comes before the page display.');
$this->assertEquals(2, $displays['page_1']['position'], 'Make sure the page display comes after the block display.');
// Ensure the view displays are in the expected order in configuration.
$this->assertEquals($expected_display_order, array_keys($view->storage->get('display')), 'The correct display names are present.');
}
/**
* Tests disabling of a display.
*/
public function testDisableDisplay(): void {
$view = $this->randomView();
$path_prefix = 'admin/structure/views/view/' . $view['id'] . '/edit';
// Verify that the disabled display css class does not appear after initial
// adding of a view.
$this->drupalGet($path_prefix);
$this->assertSession()->elementNotExists('xpath', "//div[contains(@class, 'views-display-disabled')]");
$this->assertSession()->buttonExists('edit-displays-settings-settings-content-tab-content-details-top-actions-disable');
$this->assertSession()->buttonNotExists('edit-displays-settings-settings-content-tab-content-details-top-actions-enable');
// Verify that the disabled display css class appears once the display is
// marked as such.
$this->submitForm([], 'Disable Page');
$this->assertSession()->elementExists('xpath', "//div[contains(@class, 'views-display-disabled')]");
$this->assertSession()->buttonNotExists('edit-displays-settings-settings-content-tab-content-details-top-actions-disable');
$this->assertSession()->buttonExists('edit-displays-settings-settings-content-tab-content-details-top-actions-enable');
// Verify that the disabled display css class does not appears once the
// display is enabled again.
$this->submitForm([], 'Enable Page');
$this->assertSession()->elementNotExists('xpath', "//div[contains(@class, 'views-display-disabled')]");
}
/**
* Tests views_ui_views_plugins_display_alter is altering plugin definitions.
*/
public function testDisplayPluginsAlter(): void {
$definitions = Views::pluginManager('display')->getDefinitions();
$expected = [
'route_name' => 'entity.view.edit_form',
'route_parameters_names' => ['view' => 'id'],
];
// Test the expected views_ui array exists on each definition.
foreach ($definitions as $definition) {
$this->assertSame($expected, $definition['contextual links']['entity.view.edit_form'], 'Expected views_ui array found in plugin definition.');
}
}
/**
* Tests display areas.
*/
public function testDisplayAreas(): void {
// 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 = [
'header',
'footer',
'empty',
];
// Assert that the expected text is found in each area category.
foreach ($areas as $type) {
$this->assertSession()->elementTextEquals('xpath', "//div[contains(@class, '$type')]/div", "The selected display type does not use $type plugins");
}
}
/**
* Tests the link-display setting.
*/
public function testLinkDisplay(): void {
// 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);
$this->assertSession()->elementTextEquals('xpath', "//a[contains(@href, '{$link_display_path}')]", 'None');
$this->drupalGet($link_display_path);
$this->assertSession()->checkboxChecked('edit-link-display-0');
// Test the default radio option on the link display form.
$this->drupalGet($link_display_path);
$this->submitForm(['link_display' => 'page_1'], 'Apply');
// The form redirects to the default display.
$this->drupalGet($path);
// Test that the link option summary shows the right linked display.
$this->assertSession()->elementTextEquals('xpath', "//a[contains(@href, '{$link_display_path}')]", 'Page');
$this->drupalGet($link_display_path);
$this->submitForm([
'link_display' => 'custom_url',
'link_url' => 'a-custom-url',
], 'Apply');
// The form redirects to the default display.
$this->drupalGet($path);
$this->assertSession()->linkExists('Custom URL', 0, 'The link option has custom URL as summary.');
// Test the default link_url value for new display
$this->submitForm([], 'Add Block');
$this->assertSession()->addressEquals('admin/structure/views/view/test_display/edit/block_2');
$this->clickLink('Custom URL');
$this->assertSession()->fieldValueEquals('link_url', 'a-custom-url');
}
/**
* Tests that the view status is correctly reflected on the edit form.
*/
public function testViewStatus(): void {
$view = $this->randomView();
$id = $view['id'];
// The view should initially have the enabled class on its form wrapper.
$this->drupalGet('admin/structure/views/view/' . $id);
$this->assertSession()->elementExists('xpath', "//div[contains(@class, 'views-edit-view') and contains(@class, 'enabled')]");
$view = Views::getView($id);
$view->storage->disable()->save();
// The view should now have the disabled class on its form wrapper.
$this->drupalGet('admin/structure/views/view/' . $id);
$this->assertSession()->elementExists('xpath', "//div[contains(@class, 'views-edit-view') and contains(@class, 'disabled')]");
}
/**
* Ensures that no XSS is possible for buttons.
*/
public function testDisplayTitleInButtonsXss(): void {
$xss_markup = '"><script>alert(123)</script>';
$view = $this->randomView();
$view = View::load($view['id']);
\Drupal::configFactory()->getEditable('views.settings')->set('ui.show.default_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 = Unicode::truncate($input, 25, FALSE, TRUE);
$this->assertSession()->assertEscaped($escaped);
$this->assertSession()->responseNotContains($xss_markup);
$this->drupalGet("admin/structure/views/view/{$view->id()}/edit/page_1");
$this->assertSession()->assertEscaped("View $escaped");
$this->assertSession()->responseNotContains("View $xss_markup");
$this->assertSession()->assertEscaped("Duplicate $escaped");
$this->assertSession()->responseNotContains("Duplicate $xss_markup");
$this->assertSession()->assertEscaped("Delete $escaped");
$this->assertSession()->responseNotContains("Delete $xss_markup");
}
}
/**
* Tests the action links on the edit display UI.
*/
public function testActionLinks(): void {
// 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->drupalGet($display_title_path);
$this->submitForm(['display_title' => $display_title], 'Apply');
// Ensure that the title is escaped as expected.
$this->assertSession()->assertEscaped($display_title);
$this->assertSession()->responseNotContains($display_title);
// Ensure that the dropdown buttons are displayed correctly.
$this->assertSession()->buttonExists('Duplicate ' . $display_title);
$this->assertSession()->buttonExists('Delete ' . $display_title);
$this->assertSession()->buttonExists('Disable ' . $display_title);
$this->assertSession()->buttonNotExists('Enable ' . $display_title);
// Disable the display so we can test the rendering of the "Enable" button.
$this->submitForm([], 'Disable ' . $display_title);
$this->assertSession()->buttonExists('Enable ' . $display_title);
$this->assertSession()->buttonNotExists('Disable ' . $display_title);
// Ensure that the title is escaped as expected.
$this->assertSession()->assertEscaped($display_title);
$this->assertSession()->responseNotContains($display_title);
}
/**
* Tests that the override option is hidden when it's not needed.
*/
public function testHideDisplayOverride(): void {
// Test that the override option appears with two displays.
$this->drupalGet('admin/structure/views/nojs/handler/test_display/page_1/field/title');
$this->assertSession()->pageTextContains('All displays');
// Remove a display and test if the override option is hidden.
$this->drupalGet('admin/structure/views/view/test_display/edit/block_1');
$this->submitForm([], 'Delete Block');
$this->submitForm([], 'Save');
$this->drupalGet('admin/structure/views/nojs/handler/test_display/page_1/field/title');
$this->assertSession()->pageTextNotContains('All displays');
// Test that the override option is shown when default display is on.
\Drupal::configFactory()->getEditable('views.settings')->set('ui.show.default_display', TRUE)->save();
$this->drupalGet('admin/structure/views/nojs/handler/test_display/page_1/field/title');
$this->assertSession()->pageTextContains('All displays');
// Test that the override option is shown if the current display is
// overridden so that the option to revert is available.
$this->submitForm(['override[dropdown]' => 'page_1'], 'Apply');
\Drupal::configFactory()->getEditable('views.settings')->set('ui.show.default_display', FALSE)->save();
$this->drupalGet('admin/structure/views/nojs/handler/test_display/page_1/field/title');
$this->assertSession()->pageTextContains('Revert to default');
}
}

View File

@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\views_ui\Functional;
use Drupal\language\Entity\ConfigurableLanguage;
/**
* Tests the UI for view duplicate tool.
*
* @group views_ui
*/
class DuplicateTest extends UITestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['config_translation', 'locale', 'language'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp($import_test_views = TRUE, $modules = ['views_test_config']): void {
parent::setUp($import_test_views, $modules);
$this->placeBlock('page_title_block');
}
/**
* Checks if duplicated view exists and has correct label.
*/
public function testDuplicateView(): void {
$language_manager = $this->container->get('language_manager');
ConfigurableLanguage::createFromLangcode('nl')->save();
// Create random view.
$random_view = $this->randomView();
// Add a translation to the View.
$translation = $language_manager->getLanguageConfigOverride('nl', 'views.view.' . $random_view['id']);
$translation->setData(['label' => 'NL label']);
$translation->save();
// Initialize array for duplicated view.
$view = [];
// Generate random label and id for new view.
$view['label'] = $this->randomMachineName(255);
$view['id'] = $this->randomMachineName(128);
// Duplicate view.
$this->drupalGet('admin/structure/views/view/' . $random_view['id'] . '/duplicate');
$this->submitForm($view, 'Duplicate');
// Assert that the page URL is correct.
$this->assertSession()->addressEquals('admin/structure/views/view/' . $view['id']);
// Assert that the page title is correctly displayed.
$this->assertSession()->pageTextContains($view['label']);
$copy_translation = $language_manager->getLanguageConfigOverride('nl', 'views.view.' . $view['id']);
$this->assertEquals(['label' => 'NL label'], $copy_translation->get());
}
}

View File

@@ -0,0 +1,404 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\views_ui\Functional;
use Drupal\views\Entity\View;
/**
* Tests exposed forms UI functionality.
*
* @group views_ui
* @group #slow
*/
class ExposedFormUITest extends UITestBase {
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = ['test_exposed_admin_ui'];
/**
* {@inheritdoc}
*/
protected static $modules = [
'node',
'views_ui',
'block',
'taxonomy',
'field_ui',
'datetime',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Array of error message strings raised by the grouped form.
*
* @var array
*
* @see FilterPluginBase::buildGroupValidate
*/
protected $groupFormUiErrors = [];
/**
* {@inheritdoc}
*/
protected function setUp($import_test_views = TRUE, $modules = ['views_test_config']): void {
parent::setUp($import_test_views, $modules);
$this->drupalCreateContentType(['type' => 'article']);
$this->drupalCreateContentType(['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'] = 'A value is required if the label for this item is defined.';
$this->groupFormUiErrors['missing_title'] = 'A label is required if the value for this item is defined.';
$this->groupFormUiErrors['missing_title_empty_operator'] = 'A label is required for the specified operator.';
}
/**
* Tests the admin interface of exposed filter and sort items.
*/
public function testExposedAdminUi(): void {
$edit = [];
$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', 'Expose filter');
// The first time the filter UI is displayed, the operator and the
// value forms should be shown.
$this->assertSession()->fieldValueEquals('edit-options-operator-in', 'in');
$this->assertSession()->fieldValueEquals('edit-options-operator-not-in', 'in');
$this->assertSession()->checkboxNotChecked('edit-options-value-page');
$this->assertSession()->checkboxNotChecked('edit-options-value-article');
// Click the Expose filter button.
$this->drupalGet('admin/structure/views/nojs/handler/test_exposed_admin_ui/default/filter/type');
$this->submitForm($edit, 'Expose filter');
// Check the label of the expose button.
$this->helperButtonHasLabel('edit-options-expose-button-button', 'Hide filter');
// After exposing the filter, Operator and Value should be still here.
$this->assertSession()->fieldValueEquals('edit-options-operator-in', 'in');
$this->assertSession()->fieldValueEquals('edit-options-operator-not-in', 'in');
$this->assertSession()->checkboxNotChecked('edit-options-value-page');
$this->assertSession()->checkboxNotChecked('edit-options-value-article');
// Check the validations of the filter handler.
$edit = [];
$edit['options[expose][identifier]'] = '';
$this->submitForm($edit, 'Apply');
$this->assertSession()->pageTextContains('The identifier is required if the filter is exposed.');
$edit = [];
$edit['options[expose][identifier]'] = 'value';
$this->submitForm($edit, 'Apply');
$this->assertSession()->pageTextContains('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', 'Expose sort');
$this->assertSession()->fieldNotExists('edit-options-expose-label');
$this->assertSession()->fieldNotExists('Sort field identifier');
// Un-expose the filter.
$this->drupalGet('admin/structure/views/nojs/handler/test_exposed_admin_ui/default/filter/type');
$this->submitForm([], 'Hide filter');
// After Un-exposing the filter, Operator and Value should be shown again.
$this->assertSession()->fieldValueEquals('edit-options-operator-in', 'in');
$this->assertSession()->fieldValueEquals('edit-options-operator-not-in', 'in');
$this->assertSession()->checkboxNotChecked('edit-options-value-page');
$this->assertSession()->checkboxNotChecked('edit-options-value-article');
// Click the Expose sort button.
$edit = [];
$this->drupalGet('admin/structure/views/nojs/handler/test_exposed_admin_ui/default/sort/created');
$this->submitForm($edit, 'Expose sort');
// Check the label of the expose button.
$this->helperButtonHasLabel('edit-options-expose-button-button', 'Hide sort');
$this->assertSession()->fieldValueEquals('edit-options-expose-label', 'Authored on');
$this->assertSession()->fieldValueEquals('Sort field identifier', 'created');
// 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->submitForm(['name[node_field_data.created]' => 1], 'Add and configure sort criteria');
$this->assertSession()->fieldValueEquals('options[order]', 'ASC');
// Change the order and expose the sort.
$this->submitForm(['options[order]' => 'DESC'], 'Apply');
$this->drupalGet("admin/structure/views/nojs/handler/{$view_id}/default/sort/created");
$this->submitForm([], 'Expose sort');
$this->assertSession()->fieldValueEquals('options[order]', 'DESC');
$this->assertSession()->fieldValueEquals('options[expose][label]', 'Authored on');
$this->assertSession()->fieldValueEquals('Sort field identifier', 'created');
// Change the label and try with an empty identifier.
$edit = [
'options[expose][label]' => $this->randomString(),
'options[expose][field_identifier]' => '',
];
$this->submitForm($edit, 'Apply');
$this->assertSession()->pageTextContains('Sort field identifier field is required.');
// Try with an invalid identifiers.
$edit['options[expose][field_identifier]'] = 'abc&! ###08.';
$this->submitForm($edit, 'Apply');
$this->assertSession()->pageTextContains('This identifier has illegal characters.');
$edit['options[expose][field_identifier]'] = '^abcde';
$this->submitForm($edit, 'Apply');
$this->assertSession()->pageTextContains('This identifier has illegal characters.');
// Use a valid identifier.
$edit['options[expose][field_identifier]'] = $this->randomMachineName() . '_-~.';
$this->submitForm($edit, 'Apply');
$this->submitForm([], 'Save');
// Check that the values were saved.
$display = View::load($view_id)->getDisplay('default');
$this->assertTrue($display['display_options']['sorts']['created']['exposed']);
$this->assertSame([
'label' => $edit['options[expose][label]'],
'field_identifier' => $edit['options[expose][field_identifier]'],
], $display['display_options']['sorts']['created']['expose']);
$this->assertSame('DESC', $display['display_options']['sorts']['created']['order']);
// Test the identifier uniqueness.
$this->drupalGet("admin/structure/views/nojs/handler/{$view_id}/default/sort/created_1");
$this->submitForm([], 'Expose sort');
$this->submitForm([
'options[expose][field_identifier]' => $edit['options[expose][field_identifier]'],
], 'Apply');
$this->assertSession()->pageTextContains('This identifier is already used by Content: Authored on sort handler.');
}
/**
* Tests the admin interface of exposed grouped filters.
*/
public function testGroupedFilterAdminUi(): void {
$edit = [];
$this->drupalGet('admin/structure/views/nojs/handler/test_exposed_admin_ui/default/filter/type');
// Click the Expose filter button.
$this->drupalGet('admin/structure/views/nojs/handler/test_exposed_admin_ui/default/filter/type');
$this->submitForm($edit, 'Expose filter');
// Check the label of the grouped filters button.
$this->helperButtonHasLabel('edit-options-group-button-button', 'Grouped filters');
// Click the Grouped Filters button.
$this->drupalGet('admin/structure/views/nojs/handler/test_exposed_admin_ui/default/filter/type');
$this->submitForm([], 'Grouped filters');
// After click on 'Grouped Filters', the standard operator and value should
// not be displayed.
$this->assertSession()->fieldNotExists('edit-options-operator-in');
$this->assertSession()->fieldNotExists('edit-options-operator-not-in');
$this->assertSession()->fieldNotExists('edit-options-value-page');
$this->assertSession()->fieldNotExists('edit-options-value-article');
// 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', '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 = [];
$edit["options[group_info][group_items][1][title]"] = 'Is Article';
$edit["options[group_info][group_items][1][value][article]"] = 'article';
$this->submitForm($edit, 'Apply');
$this->assertSession()->addressEquals('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 = [];
$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->submitForm($edit, 'Apply');
$this->assertSession()->addressEquals('admin/structure/views/view/test_exposed_admin_ui/edit/default');
$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 = [];
$edit["options[group_info][group_items][1][title]"] = 'No body';
$edit["options[group_info][group_items][1][operator]"] = 'empty';
$this->submitForm($edit, 'Apply');
$this->assertSession()->addressEquals('admin/structure/views/view/test_exposed_admin_ui/edit/default');
$this->assertNoGroupedFilterErrors();
// Ensure the string "0" can be used as a value for numeric filters.
$this->drupalGet('admin/structure/views/nojs/add-handler/test_exposed_admin_ui/default/filter');
$this->submitForm(['name[node_field_data.nid]' => TRUE], 'Add and configure filter criteria');
$this->submitForm([], 'Expose filter');
$this->submitForm([], 'Grouped filters');
$edit = [];
$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->submitForm($edit, 'Apply');
$this->assertSession()->addressEquals('admin/structure/views/view/test_exposed_admin_ui/edit/default');
$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->submitForm($edit, 'Apply');
$this->assertSession()->addressEquals('admin/structure/views/view/test_exposed_admin_ui/edit/default');
$this->assertNoGroupedFilterErrors();
}
public function testGroupedFilterAdminUiErrors(): void {
// Select the empty operator without a title specified.
$this->drupalGet('admin/structure/views/nojs/handler/test_exposed_admin_ui/default/filter/body_value');
$edit = [];
$edit["options[group_info][group_items][1][title]"] = '';
$edit["options[group_info][group_items][1][operator]"] = 'empty';
$this->submitForm($edit, 'Apply');
$this->assertSession()->pageTextContains($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->drupalGet('admin/structure/views/nojs/handler/test_exposed_admin_ui/default/filter/type');
$this->submitForm([], 'Expose filter');
$this->drupalGet('admin/structure/views/nojs/handler/test_exposed_admin_ui/default/filter/type');
$this->submitForm([], 'Grouped filters');
$edit = [];
$edit["options[group_info][group_items][1][title]"] = 'Is Article';
$this->submitForm($edit, 'Apply');
$this->assertSession()->pageTextContains($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 = [];
$edit["options[group_info][group_items][1][title]"] = '';
$edit["options[group_info][group_items][1][value][article]"] = 'article';
$this->submitForm($edit, 'Apply');
$this->assertSession()->pageTextContains($this->groupFormUiErrors['missing_title']);
}
/**
* Asserts that there are no Grouped Filters errors.
*
* @param string $message
* The assert message.
*
* @internal
*/
protected function assertNoGroupedFilterErrors(string $message = ''): void {
foreach ($this->groupFormUiErrors as $error) {
if (empty($message)) {
$this->assertSession()->responseNotContains($error);
}
}
}
/**
* Tests the configuration of grouped exposed filters.
*/
public function testExposedGroupedFilter(): void {
// Click the Expose filter button.
$this->drupalGet('admin/structure/views/nojs/handler/test_exposed_admin_ui/default/filter/type');
$this->submitForm([], 'Expose filter');
// Select 'Grouped filters' radio button.
$this->drupalGet('admin/structure/views/nojs/handler/test_exposed_admin_ui/default/filter/type');
$this->submitForm([], 'Grouped filters');
// Add 3 groupings.
$edit = [
'options[group_button][radios][radios]' => 1,
'options[group_info][group_items][1][title]' => '1st',
'options[group_info][group_items][1][value][all]' => 'all',
'options[group_info][group_items][2][title]' => '2nd',
'options[group_info][group_items][2][value][article]' => 'article',
'options[group_info][group_items][3][title]' => '3rd',
'options[group_info][group_items][3][value][page]' => 'page',
'options[group_info][default_group]' => '3',
];
// Apply the filter settings.
$this->submitForm($edit, 'Apply');
// Check that the view is saved without errors.
$this->submitForm([], 'Save');
$this->assertSession()->statusCodeEquals(200);
// Check the default filter value.
$this->drupalGet('test_exposed_admin_ui');
$this->assertSession()->fieldValueEquals('type', '3');
// Enable "Allow multiple selections" option and set a default group.
$this->drupalGet('admin/structure/views/nojs/handler/test_exposed_admin_ui/default/filter/type');
$edit['options[group_info][multiple]'] = 1;
$edit['options[group_info][default_group_multiple][1]'] = 1;
$this->submitForm($edit, 'Apply');
$this->submitForm([], 'Save');
// Check the default filter values again.
$this->drupalGet('test_exposed_admin_ui');
$this->assertSession()->checkboxChecked('type[1]');
$this->assertSession()->checkboxNotChecked('type[2]');
$this->assertSession()->checkboxNotChecked('type[3]');
// Click the Expose filter button.
$this->drupalGet('admin/structure/views/nojs/add-handler/test_exposed_admin_ui/default/filter');
$this->submitForm(['name[node_field_data.status]' => 1], 'Add and configure filter criteria');
$this->drupalGet('admin/structure/views/nojs/handler/test_exposed_admin_ui/default/filter/status');
$this->submitForm([], 'Expose filter');
// Select 'Grouped filters' radio button.
$this->drupalGet('admin/structure/views/nojs/handler/test_exposed_admin_ui/default/filter/status');
$this->submitForm([], 'Grouped filters');
// Add 3 groupings.
$edit = [
'options[group_button][radios][radios]' => 1,
'options[group_info][group_items][1][title]' => 'Any',
'options[group_info][group_items][1][value]' => 'All',
'options[group_info][group_items][2][title]' => 'Published',
'options[group_info][group_items][2][value]' => 1,
'options[group_info][group_items][3][title]' => 'Unpublished',
'options[group_info][group_items][3][value]' => 0,
'options[group_info][default_group]' => 2,
];
// Apply the filter settings.
$this->submitForm($edit, 'Apply');
// Check that the view is saved without errors.
$this->submitForm([], 'Save');
$this->assertSession()->statusCodeEquals(200);
$this->drupalGet('admin/structure/views/nojs/handler/test_exposed_admin_ui/default/filter/status');
// Assert the same settings defined before still are there.
$this->assertSession()->checkboxChecked('edit-options-group-info-group-items-1-value-all');
$this->assertSession()->checkboxChecked('edit-options-group-info-group-items-2-value-1');
$this->assertSession()->checkboxChecked('edit-options-group-info-group-items-3-value-0');
// Check the default filter value.
$this->drupalGet('test_exposed_admin_ui');
$this->assertSession()->fieldValueEquals('status', '2');
// Enable "Allow multiple selections" option and set a default group.
$this->drupalGet('admin/structure/views/nojs/handler/test_exposed_admin_ui/default/filter/status');
$edit['options[group_info][multiple]'] = 1;
$edit['options[group_info][default_group_multiple][3]'] = 1;
$this->submitForm($edit, 'Apply');
$this->submitForm([], 'Save');
// Check the default filter value again.
$this->drupalGet('test_exposed_admin_ui');
$this->assertSession()->checkboxNotChecked('status[1]');
$this->assertSession()->checkboxNotChecked('status[2]');
$this->assertSession()->checkboxChecked('status[3]');
}
}

View File

@@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\views_ui\Functional;
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 {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = ['test_view'];
/**
* Tests the UI of field handlers.
*/
public function testFieldUI(): void {
// Ensure the field is not marked as hidden on the first run.
$this->drupalGet('admin/structure/views/view/test_view/edit');
$this->assertSession()->pageTextContains('Views test: Name');
$this->assertSession()->pageTextNotContains('Views test: Name [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->drupalGet($edit_handler_url);
$this->submitForm(['options[exclude]' => TRUE], 'Apply');
$this->assertSession()->pageTextContains('Views test: Name [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);
$xpath = '//details[@id="edit-options-alter-help"]/ul/li';
$this->assertSession()->elementTextEquals('xpath', $xpath, '{{ age }} == Age');
$edit_handler_url = 'admin/structure/views/nojs/handler/test_view/default/field/id';
$this->drupalGet($edit_handler_url);
$this->assertSession()->elementTextEquals('xpath', "{$xpath}[1]", '{{ age }} == Age');
$this->assertSession()->elementTextEquals('xpath', "{$xpath}[2]", '{{ id }} == ID');
$edit_handler_url = 'admin/structure/views/nojs/handler/test_view/default/field/name';
$this->drupalGet($edit_handler_url);
$this->assertSession()->elementTextEquals('xpath', "{$xpath}[1]", '{{ age }} == Age');
$this->assertSession()->elementTextEquals('xpath', "{$xpath}[2]", '{{ id }} == ID');
$this->assertSession()->elementTextEquals('xpath', "{$xpath}[3]", '{{ name }} == Name');
$this->assertSession()->elementNotExists('xpath', '//details[@id="edit-options-more"]');
// Ensure that dialog titles are not escaped.
$edit_groupby_url = 'admin/structure/views/nojs/handler/test_view/default/field/name';
$this->assertSession()->linkByHrefNotExists($edit_groupby_url, 0, 'No aggregation link found.');
// Enable aggregation on the view.
$edit = [
'group_by' => TRUE,
];
$this->drupalGet('/admin/structure/views/nojs/display/test_view/default/group_by');
$this->submitForm($edit, 'Apply');
$this->assertSession()->linkByHrefExists($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->getSession()->getPage()->getContent());
$this->assertEquals('Configure aggregation settings for field Views test: Name', $data[3]['dialogOptions']['title']);
}
/**
* Tests the field labels.
*/
public function testFieldLabel(): void {
// Create a view with unformatted style and make sure the fields have no
// labels by default.
$view = [];
$view['label'] = $this->randomMachineName(16);
$view['id'] = $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->drupalGet('admin/structure/views/add');
$this->submitForm($view, 'Save and edit');
$view = Views::getView($view['id']);
$view->initHandlers();
$this->assertEquals('', $view->field['title']->options['label'], 'The field label for normal styles are empty.');
}
}

View File

@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\views_ui\Functional;
/**
* 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 = ['test_view'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests the filter boolean UI.
*/
public function testFilterBooleanUI(): void {
$this->drupalGet('admin/structure/views/nojs/add-handler/test_view/default/filter');
$this->submitForm(['name[views_test_data.status]' => TRUE], 'Add and configure filter criteria');
// Check the field widget label. 'title' should be used as a fallback.
$result = $this->cssSelect('#edit-options-value--wrapper legend span');
$this->assertEquals('Status', $result[0]->getHtml());
// Ensure that the operator and the filter value are displayed using correct
// layout.
$this->assertSession()->elementExists('css', '.views-left-30 .form-item-options-operator');
$this->assertSession()->elementExists('css', '.views-right-70 .form-item-options-value');
$this->submitForm([], 'Expose filter');
$this->submitForm([], 'Grouped filters');
$edit = [];
$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->submitForm($edit, '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->assertEquals('checked', $result[1]->getAttribute('checked'));
$result = $this->xpath('//input[@name="options[group_info][group_items][2][value]"]');
$this->assertEquals('checked', $result[2]->getAttribute('checked'));
$result = $this->xpath('//input[@name="options[group_info][group_items][3][value]"]');
$this->assertEquals('checked', $result[1]->getAttribute('checked'));
// Test that there is a remove link for each group.
$this->assertCount(3, $this->cssSelect('a.views-remove-link'));
// Test selecting a default and removing an item.
$edit = [];
$edit['options[group_info][default_group]'] = 2;
$edit['options[group_info][group_items][3][remove]'] = 1;
$this->submitForm($edit, 'Apply');
$this->drupalGet('admin/structure/views/nojs/handler/test_view/default/filter/status');
$this->assertSession()->fieldValueEquals('options[group_info][default_group]', 2);
$this->assertSession()->fieldNotExists('options[group_info][group_items][3][remove]');
}
}

View File

@@ -0,0 +1,152 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\views_ui\Functional;
use Drupal\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 = ['test_view'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests the filter numeric UI.
*/
public function testFilterNumericUI(): void {
// Add a page display to the test_view to be able to test the filtering.
$path = 'test_view-path';
$this->drupalGet('admin/structure/views/view/test_view/edit');
$this->submitForm([], 'Add Page');
$this->drupalGet('admin/structure/views/nojs/display/test_view/page_1/path');
$this->submitForm(['path' => $path], 'Apply');
$this->submitForm([], 'Save');
$this->drupalGet('admin/structure/views/nojs/add-handler/test_view/default/filter');
$this->submitForm(['name[views_test_data.age]' => TRUE], 'Add and configure filter criteria');
$this->submitForm([], 'Expose filter');
$this->submitForm([], 'Grouped filters');
$edit = [];
$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->submitForm($edit, 'Apply');
$this->drupalGet('admin/structure/views/nojs/handler/test_view/default/filter/age');
foreach ($edit as $name => $value) {
$this->assertSession()->fieldValueEquals($name, $value);
}
$this->drupalGet('admin/structure/views/view/test_view');
$this->submitForm([], 'Save');
$this->assertConfigSchemaByName('views.view.test_view');
// Test that the exposed filter works as expected.
$this->drupalGet('test_view-path');
$this->assertSession()->pageTextContains('John');
$this->assertSession()->pageTextContains('Paul');
$this->assertSession()->pageTextContains('Ringo');
$this->assertSession()->pageTextContains('George');
$this->assertSession()->pageTextContains('Meredith');
$this->submitForm(['age' => '2'], 'Apply');
$this->assertSession()->pageTextContains('John');
$this->assertSession()->pageTextContains('Paul');
$this->assertSession()->pageTextNotContains('Ringo');
$this->assertSession()->pageTextContains('George');
$this->assertSession()->pageTextNotContains('Meredith');
// Change the filter to a single filter to test the schema when the operator
// is not exposed.
$this->drupalGet('admin/structure/views/nojs/handler/test_view/default/filter/age');
$this->submitForm([], 'Single filter');
$edit = [];
$edit['options[value][value]'] = 25;
$this->submitForm($edit, 'Apply');
$this->drupalGet('admin/structure/views/view/test_view');
$this->submitForm([], 'Save');
$this->assertConfigSchemaByName('views.view.test_view');
// Test that the filter works as expected.
$this->drupalGet('test_view-path');
$this->assertSession()->pageTextContains('John');
$this->assertSession()->pageTextNotContains('Paul');
$this->assertSession()->pageTextNotContains('Ringo');
$this->assertSession()->pageTextNotContains('George');
$this->assertSession()->pageTextNotContains('Meredith');
$this->submitForm(['age' => '26'], 'Apply');
$this->assertSession()->pageTextNotContains('John');
$this->assertSession()->pageTextContains('Paul');
$this->assertSession()->pageTextNotContains('Ringo');
$this->assertSession()->pageTextNotContains('George');
$this->assertSession()->pageTextNotContains('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 = [];
$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->submitForm($edit, 'Apply');
$this->drupalGet('admin/structure/views/view/test_view');
$this->submitForm([], 'Save');
$this->assertConfigSchemaByName('views.view.test_view');
$this->submitForm([], 'Update preview');
// Check the field (wrapper) label.
$this->assertSession()->elementTextContains('css', 'fieldset#edit-age-wrapper legend', 'Age between');
// Check the min/max labels.
$this->assertSession()->elementsCount('xpath', '//fieldset[contains(@id, "edit-age-wrapper")]//label[contains(@for, "edit-age-min") and contains(text(), "Min")]', 1);
$this->assertSession()->elementsCount('xpath', '//fieldset[contains(@id, "edit-age-wrapper")]//label[contains(@for, "edit-age-max") and contains(text(), "Max")]', 1);
// Check that the description is shown in the right place.
$this->assertEquals('Description of the exposed filter', trim($this->cssSelect('#edit-age-wrapper--description')[0]->getText()));
// Change to an operation that only requires one form element ('>').
$this->drupalGet('admin/structure/views/nojs/handler/test_view/default/filter/age');
$edit = [];
$edit['options[expose][label]'] = 'Age greater than';
$edit['options[expose][description]'] = 'Description of the exposed filter';
$edit['options[operator]'] = '>';
$edit['options[value][value]'] = 1000;
$this->submitForm($edit, 'Apply');
$this->drupalGet('admin/structure/views/view/test_view');
$this->submitForm([], 'Save');
$this->assertConfigSchemaByName('views.view.test_view');
$this->submitForm([], 'Update preview');
// Make sure the label is visible and that there's no fieldset wrapper.
$this->assertSession()->elementsCount('xpath', '//label[contains(@for, "edit-age") and contains(text(), "Age greater than")]', 1);
$this->assertSession()->elementNotExists('xpath', '//fieldset[contains(@id, "edit-age-wrapper")]');
}
}

View File

@@ -0,0 +1,142 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\views_ui\Functional;
/**
* Tests for the filters from the UI.
*
* @group views_ui
*/
class FilterUITest extends UITestBase {
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = ['test_filter_in_operator_ui', 'test_filter_groups'];
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['views_ui', 'node'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp($import_test_views = TRUE, $modules = ['views_test_config']): void {
parent::setUp($import_test_views, $modules);
$this->drupalCreateContentType(['type' => 'page']);
}
/**
* Tests that an option for a filter is saved as expected from the UI.
*/
public function testFilterInOperatorUi(): void {
$admin_user = $this->drupalCreateUser([
'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->assertSession()->fieldValueEquals('options[expose][reduce]', FALSE);
// Select "Limit list to selected items" option and apply.
$edit = [
'options[expose][reduce]' => TRUE,
];
$this->drupalGet($path);
$this->submitForm($edit, 'Apply');
// Verifies that the option was saved as expected.
$this->drupalGet($path);
$this->assertSession()->fieldValueEquals('options[expose][reduce]', TRUE);
}
/**
* Tests the filters from the UI.
*/
public function testFiltersUI(): void {
$admin_user = $this->drupalCreateUser([
'administer views',
'administer site configuration',
]);
$this->drupalLogin($admin_user);
$this->drupalGet('admin/structure/views/view/test_filter_groups');
$this->assertSession()->linkExists('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->assertSession()->elementNotExists('xpath', '//span[text()="Group 3"]');
// Create 2 new groups.
$this->submitForm([], 'Create new filter group');
$this->submitForm([], 'Create new filter group');
// Remove the new group 3.
$this->submitForm([], 'Remove group 3');
// Verify that the group 4 is now named as 3.
$this->assertSession()->responseContains('<span>Group 3</span>');
// Remove the group 3 again.
$this->submitForm([], 'Remove group 3');
// Group 3 now does not exist.
$this->assertSession()->elementNotExists('xpath', '//span[text()="Group 3"]');
}
/**
* Tests the identifier settings and restrictions.
*/
public function testFilterIdentifier(): void {
$admin_user = $this->drupalCreateUser([
'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 = [
'options[expose][identifier]' => '',
];
$this->drupalGet($path);
$this->submitForm($edit, 'Apply');
$this->assertSession()->pageTextContains('The identifier is required if the filter is exposed.');
// Set the identifier to 'value'.
$edit = [
'options[expose][identifier]' => 'value',
];
$this->drupalGet($path);
$this->submitForm($edit, 'Apply');
$this->assertSession()->pageTextContains('This identifier is not allowed.');
// Try a few restricted values for the identifier.
foreach (['value value', 'value^value'] as $identifier) {
$edit = [
'options[expose][identifier]' => $identifier,
];
$this->drupalGet($path);
$this->submitForm($edit, 'Apply');
$this->assertSession()->pageTextContains('This identifier has illegal characters.');
}
}
}

View File

@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\views_ui\Functional;
use Drupal\Tests\system\Functional\Module\GenericModuleTestBase;
/**
* Generic module test for views_ui.
*
* @group views_ui
*/
class GenericTest extends GenericModuleTestBase {}

View File

@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\views_ui\Functional;
/**
* Tests UI of aggregate functionality..
*
* @group views_ui
*/
class GroupByTest extends UITestBase {
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = ['test_views_groupby_save'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests whether basic saving works.
*
* @todo This should check the change of the settings as well.
*/
public function testGroupBySave(): void {
$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->assertSession()->linkByHrefNotExists($edit_groupby_url, 0, 'No aggregation link found.');
// Enable aggregation on the view.
$edit = [
'group_by' => TRUE,
];
$this->drupalGet('admin/structure/views/nojs/display/test_views_groupby_save/default/group_by');
$this->submitForm($edit, 'Apply');
$this->assertSession()->linkByHrefExists($edit_groupby_url, 0, 'Aggregation link found.');
// Change the groupby type in the UI.
$this->drupalGet($edit_groupby_url);
$this->submitForm(['options[group_type]' => 'count'], 'Apply');
$this->assertSession()->linkExists('COUNT(Views test: ID)', 0, 'The count setting is displayed in the UI');
$this->submitForm([], 'Save');
$view = $this->container->get('entity_type.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->assertEquals('count', $display['display_options']['fields']['id']['group_type'], 'Count groupby_type was saved on the view.');
}
}

View File

@@ -0,0 +1,307 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\views_ui\Functional;
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
* @group #slow
* @see \Drupal\views\Plugin\views\HandlerBase
*/
class HandlerTest extends UITestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['node_test_views'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = ['test_view_empty', 'test_view_broken', 'node', 'test_node_view'];
/**
* {@inheritdoc}
*/
protected function setUp($import_test_views = TRUE, $modules = ['views_test_config']): void {
parent::setUp($import_test_views, $modules);
$this->placeBlock('page_title_block');
ViewTestData::createTestViews(static::class, ['node_test_views']);
}
/**
* Overrides \Drupal\views\Tests\ViewTestBase::schemaDefinition().
*
* Adds a uid column to test the relationships.
*
* @internal
*/
protected function schemaDefinition() {
$schema = parent::schemaDefinition();
$schema['views_test_data']['fields']['uid'] = [
'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'] = [
'title' => 'UID',
'help' => 'The test data UID',
'relationship' => [
'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'] = '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(): void {
$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, ['header', 'footer', 'empty'])) {
$this->drupalGet($add_handler_url);
$this->submitForm([
'name[views.area]' => TRUE,
], 'Add and configure ' . $type_info['ltitle']);
$id = 'area';
$edit_handler_url = "admin/structure/views/nojs/handler/test_view_empty/default/$type/$id";
}
elseif ($type == 'relationship') {
$this->drupalGet($add_handler_url);
$this->submitForm([
'name[views_test_data.uid]' => TRUE,
], 'Add and configure ' . $type_info['ltitle']);
$id = 'uid';
$edit_handler_url = "admin/structure/views/nojs/handler/test_view_empty/default/$type/$id";
}
else {
$this->drupalGet($add_handler_url);
$this->submitForm([
'name[views_test_data.job]' => TRUE,
], 'Add and configure ' . $type_info['ltitle']);
$id = 'job';
$edit_handler_url = "admin/structure/views/nojs/handler/test_view_empty/default/$type/$id";
}
// Verify that the user got redirected to the handler edit form.
$this->assertSession()->addressEquals($edit_handler_url);
$random_label = $this->randomMachineName();
$this->submitForm(['options[admin_label]' => $random_label], 'Apply');
// Verify that the user got redirected to the views edit form.
$this->assertSession()->addressEquals('admin/structure/views/view/test_view_empty/edit/default');
$this->assertSession()->linkByHrefExists($edit_handler_url, 0, 'The handler edit link appears in the UI.');
// Test that the handler edit link has the right label.
$this->assertSession()->elementExists('xpath', "//a[starts-with(normalize-space(text()), '{$random_label}')]");
// Save the view and have a look whether the handler was added as expected.
$this->submitForm([], 'Save');
$view = $this->container->get('entity_type.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->drupalGet($edit_handler_url);
$this->submitForm([], 'Remove');
$this->assertSession()->linkByHrefNotExists($edit_handler_url, 0, 'The handler edit link does not appears in the UI after removing.');
$this->submitForm([], 'Save');
$view = $this->container->get('entity_type.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->drupalGet($add_handler_url);
$this->submitForm([
'name[views_test_data.uid]' => TRUE,
], 'Add and configure ' . $type_info['ltitle']);
$add_handler_url = "admin/structure/views/nojs/add-handler/test_view_empty/default/field";
$type_info = $handler_types['field'];
$this->drupalGet($add_handler_url);
$this->submitForm([
'name[users_field_data.name]' => TRUE,
], 'Add and configure ' . $type_info['ltitle']);
$id = 'name';
$edit_handler_url = "admin/structure/views/nojs/handler/test_view_empty/default/field/$id";
// Verify that the user got redirected to the handler edit form.
$this->assertSession()->addressEquals($edit_handler_url);
$this->assertSession()->fieldValueEquals('options[relationship]', 'uid');
$this->submitForm([], 'Apply');
$this->submitForm([], 'Save');
$view = $this->container->get('entity_type.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(): void {
// 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->assertSession()->assertEscaped('The <em>giraffe"</em> label <script>alert("the return of the xss")</script>');
$this->assertSession()->assertEscaped('Appears in: page, article. Also known as: Content: The giraffe" label');
}
/**
* Tests broken handlers.
*/
public function testBrokenHandlers(): void {
$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";
$text = 'Broken/missing handler';
// Test that the handler edit link is present.
$this->assertSession()->elementsCount('xpath', "//a[contains(@href, '{$href}')]", 1);
$result = $this->assertSession()->elementTextEquals('xpath', "//a[contains(@href, '{$href}')]", $text);
$this->drupalGet($href);
$this->assertSession()->elementTextContains('xpath', '//h1', $text);
$original_configuration = [
'field' => 'id_broken',
'id' => 'id_broken',
'relationship' => 'none',
'table' => 'views_test_data',
'plugin_id' => 'numeric',
];
foreach ($original_configuration as $key => $value) {
$this->assertSession()->pageTextContains($key . ': ' . $value);
}
}
}
/**
* Ensures that neither node type or node ID appears multiple times.
*
* @see \Drupal\views\EntityViewsData
*/
public function testNoDuplicateFields(): void {
$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(): void {
// 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->assertSession()->pageTextNotContains('Error: missing help');
$this->assertSession()->responseContains('<td class="description"></td>');
// Test that no error message is shown for other fields.
$this->drupalGet('admin/structure/views/nojs/add-handler/test_view_empty/default/field');
$this->assertSession()->pageTextNotContains('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.
*
* @internal
*/
public function assertNoDuplicateField(string $field_name, string $entity_type): void {
$this->assertSession()->elementsCount('xpath', '//td[.="' . $entity_type . '"]/preceding-sibling::td[@class="title" and .="' . $field_name . '"]', 1);
}
}

View File

@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\views_ui\Functional;
/**
* Tests configuration schema against new views.
*
* @group views_ui
*/
class NewViewConfigSchemaTest extends UITestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = [
'views_ui',
'node',
'comment',
'file',
'taxonomy',
'dblog',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests creating brand new views.
*/
public function testNewViews(): void {
$this->drupalLogin($this->drupalCreateUser(['administer views']));
// Create views with all core Views wizards.
$wizards = [
// Wizard with their own classes.
'node',
'node_revision',
'users',
'comment',
'file_managed',
'taxonomy_term',
'watchdog',
];
foreach ($wizards as $wizard_key) {
$edit = [];
$edit['label'] = $this->randomString();
$edit['id'] = $this->randomMachineName();
$edit['show[wizard_key]'] = $wizard_key;
$edit['description'] = $this->randomString();
$this->drupalGet('admin/structure/views/add');
$this->submitForm($edit, 'Save and edit');
}
}
}

View File

@@ -0,0 +1,225 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\views_ui\Functional;
/**
* Tests that displays can be correctly overridden via the user interface.
*
* @group views_ui
* @group #slow
*/
class OverrideDisplaysTest extends UITestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp($import_test_views = TRUE, $modules = ['views_test_config']): void {
parent::setUp($import_test_views, $modules);
$this->drupalPlaceBlock('page_title_block');
}
/**
* Tests that displays can be overridden via the UI.
*/
public function testOverrideDisplays(): void {
// Create a basic view that shows all content, with a page and a block
// display.
$view['label'] = $this->randomMachineName(16);
$view['id'] = $this->randomMachineName(16);
$view['page[create]'] = 1;
$view['page[path]'] = $this->randomMachineName(16);
$view['block[create]'] = 1;
$view_path = $view['page[path]'];
$this->drupalGet('admin/structure/views/add');
$this->submitForm($view, '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 = [];
$edit['title'] = $original_title = $this->randomMachineName(16);
$edit['override[dropdown]'] = 'default';
$this->drupalGet("admin/structure/views/nojs/display/{$view['id']}/page_1/title");
$this->submitForm($edit, 'Apply');
$this->drupalGet("admin/structure/views/view/{$view['id']}/edit/page_1");
$this->submitForm([], 'Save');
// Add a node that will appear in the view, so that the block will actually
// be displayed.
$this->drupalCreateContentType(['type' => 'page']);
$this->drupalCreateNode();
// Make sure the title appears in the page.
$this->drupalGet($view_path);
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains($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->clickLink('Place block');
$this->assertSession()->pageTextContains($view['label']);
// Place the block.
$this->container->get('plugin.manager.block')->clearCachedDefinitions();
$this->drupalPlaceBlock("views_block:{$view['id']}-block_1");
// Make sure the title appears in the block.
$this->drupalGet('');
$this->assertSession()->pageTextContains($original_title);
// Change the title for the page display only, and make sure that the
// original title still appears on the page.
$edit = [];
$edit['title'] = $new_title = $this->randomMachineName(16);
$edit['override[dropdown]'] = 'page_1';
$this->drupalGet("admin/structure/views/nojs/display/{$view['id']}/page_1/title");
$this->submitForm($edit, 'Apply');
$this->drupalGet("admin/structure/views/view/{$view['id']}/edit/page_1");
$this->submitForm([], 'Save');
$this->drupalGet($view_path);
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains($new_title);
$this->assertSession()->pageTextContains($original_title);
}
/**
* Tests that the wizard correctly sets up default and overridden displays.
*/
public function testWizardMixedDefaultOverriddenDisplays(): void {
// 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'] = $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->drupalGet('admin/structure/views/add');
$this->submitForm($view, 'Save and edit');
// Add a node that will appear in the view, so that the block will actually
// be displayed.
$this->drupalCreateContentType(['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->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains($view['page[title]']);
$this->assertSession()->pageTextNotContains($view['block[title]']);
$this->drupalGet($view['page[feed_properties][path]']);
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->responseContains($view['page[title]']);
$this->assertSession()->responseNotContains($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->clickLink('Place block');
$this->assertSession()->pageTextContains($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->container->get('plugin.manager.block')->clearCachedDefinitions();
$this->drupalPlaceBlock("views_block:{$view['id']}-block_1", [
'visibility' => [
'request_path' => [
'pages' => '/' . $view['page[path]'],
'negate' => TRUE,
],
],
]);
$this->drupalGet('');
$this->assertSession()->pageTextContains($view['block[title]']);
$this->assertSession()->pageTextNotContains($view['page[title]']);
// Edit the page and change the title. This should automatically change
// the feed's title also, but not the block.
$edit = [];
$edit['title'] = $new_default_title = $this->randomMachineName(16);
$this->drupalGet("admin/structure/views/nojs/display/{$view['id']}/page_1/title");
$this->submitForm($edit, 'Apply');
$this->drupalGet("admin/structure/views/view/{$view['id']}/edit/page_1");
$this->submitForm([], 'Save');
$this->drupalGet($view['page[path]']);
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains($new_default_title);
$this->assertSession()->pageTextNotContains($view['page[title]']);
$this->assertSession()->pageTextNotContains($view['block[title]']);
$this->drupalGet($view['page[feed_properties][path]']);
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->responseContains($new_default_title);
$this->assertSession()->responseNotContains($view['page[title]']);
$this->assertSession()->responseNotContains($view['block[title]']);
$this->drupalGet('');
$this->assertSession()->pageTextNotContains($new_default_title);
$this->assertSession()->pageTextNotContains($view['page[title]']);
$this->assertSession()->pageTextContains($view['block[title]']);
// Edit the block and change the title. This should automatically change
// the block title only, and leave the defaults alone.
$edit = [];
$edit['title'] = $new_block_title = $this->randomMachineName(16);
$this->drupalGet("admin/structure/views/nojs/display/{$view['id']}/block_1/title");
$this->submitForm($edit, 'Apply');
$this->drupalGet("admin/structure/views/view/{$view['id']}/edit/block_1");
$this->submitForm([], 'Save');
$this->drupalGet($view['page[path]']);
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains($new_default_title);
$this->drupalGet($view['page[feed_properties][path]']);
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->responseContains($new_default_title);
$this->assertSession()->responseNotContains($new_block_title);
$this->drupalGet('');
$this->assertSession()->pageTextContains($new_block_title);
$this->assertSession()->pageTextNotContains($view['block[title]']);
}
/**
* Tests that the revert to all displays select-option works as expected.
*/
public function testRevertAllDisplays(): void {
// 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'] = $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->drupalGet('admin/structure/views/add');
$this->submitForm($view, '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 = [];
$edit['title'] = $this->randomMachineName();
$edit['override[dropdown]'] = 'default_revert';
$this->drupalGet("admin/structure/views/nojs/display/{$view['id']}/block_1/title");
$this->submitForm($edit, 'Apply');
$this->drupalGet("admin/structure/views/view/{$view['id']}/edit/block_1");
$this->submitForm([], 'Save');
$this->assertSession()->pageTextContains($view['page[title]']);
}
}

View File

@@ -0,0 +1,207 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\views_ui\Functional;
/**
* Tests the UI preview functionality.
*
* @group views_ui
* @group #slow
*/
class PreviewTest extends UITestBase {
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = [
'test_preview',
'test_preview_error',
'test_pager_full',
'test_mini_pager',
'test_click_sort',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests contextual links in the preview form.
*/
public function testPreviewContextual(): void {
\Drupal::service('module_installer')->install(['contextual']);
$this->resetAll();
$this->drupalGet('admin/structure/views/view/test_preview/edit');
$this->assertSession()->statusCodeEquals(200);
$this->submitForm($edit = [], 'Update preview');
// Verify that the contextual link to add a new field is shown.
$selector = $this->assertSession()->buildXPathQuery('//div[@id="views-live-preview"]//ul[contains(@class, :ul-class)]/li/a[contains(@href, :href)]', [
':ul-class' => 'contextual-links',
':href' => '/admin/structure/views/nojs/add-handler/test_preview/default/filter',
]);
$this->assertSession()->elementsCount('xpath', $selector, 1);
$this->submitForm(['view_args' => '100'], 'Update preview');
// Test that area text and exposed filters are present and rendered.
$this->assertSession()->fieldExists('id');
$this->assertSession()->pageTextContains('Test header text');
$this->assertSession()->pageTextContains('Test footer text');
$this->assertSession()->pageTextContains('Test empty text');
$this->submitForm(['view_args' => '0'], 'Update preview');
// Test that area text and exposed filters are present and rendered.
$this->assertSession()->fieldExists('id');
$this->assertSession()->pageTextContains('Test header text');
$this->assertSession()->pageTextContains('Test footer text');
$this->assertSession()->pageTextContains('Test empty text');
}
/**
* Tests arguments in the preview form.
*/
public function testPreviewUI(): void {
$this->drupalGet('admin/structure/views/view/test_preview/edit');
$this->assertSession()->statusCodeEquals(200);
$this->submitForm($edit = [], 'Update preview');
$selector = '//div[@class = "views-row"]';
$this->assertSession()->elementsCount('xpath', $selector, 5);
// Filter just the first result.
$this->submitForm($edit = ['view_args' => '1'], 'Update preview');
$this->assertSession()->elementsCount('xpath', $selector, 1);
// Filter for no results.
$this->submitForm($edit = ['view_args' => '100'], 'Update preview');
$this->assertSession()->elementNotExists('xpath', $selector);
// Test that area text and exposed filters are present and rendered.
$this->assertSession()->fieldExists('id');
$this->assertSession()->pageTextContains('Test header text');
$this->assertSession()->pageTextContains('Test footer text');
$this->assertSession()->pageTextContains('Test empty text');
// Test feed preview.
$view = [];
$view['label'] = $this->randomMachineName(16);
$view['id'] = $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->drupalGet('admin/structure/views/add');
$this->submitForm($view, 'Save and edit');
$this->clickLink('Feed');
$this->submitForm([], 'Update preview');
$this->assertSession()->elementTextContains('xpath', '//div[@id="views-live-preview"]/pre', '<title>' . $view['page[title]'] . '</title>');
// 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->submitForm($edit = ['view_args' => '100'], 'Update preview');
$this->assertSession()->pageTextContains('Query build time');
$this->assertSession()->pageTextContains('Query execute time');
$this->assertSession()->pageTextContains('View render time');
$this->assertSession()->responseNotContains('<strong>Query</strong>');
// Statistics and query.
$settings->set('ui.show.sql_query.enabled', TRUE)->save();
$this->submitForm($edit = ['view_args' => '100'], 'Update preview');
$this->assertSession()->pageTextContains('Query build time');
$this->assertSession()->pageTextContains('Query execute time');
$this->assertSession()->pageTextContains('View render time');
$this->assertSession()->responseContains('<strong>Query</strong>');
$query_string = <<<SQL
SELECT "views_test_data"."name" AS "views_test_data_name"
FROM
{views_test_data} "views_test_data"
WHERE (views_test_data.id = '100')
SQL;
$this->assertSession()->assertEscaped($query_string);
// Test that the statistics and query are rendered above the preview.
$this->assertLessThan(strpos($this->getSession()->getPage()->getContent(), 'js-view-dom-id'), strpos($this->getSession()->getPage()->getContent(), 'views-query-info'));
// Test that statistics and query rendered below the preview.
$settings->set('ui.show.sql_query.where', 'below')->save();
$this->submitForm($edit = ['view_args' => '100'], 'Update preview');
$this->assertLessThan(strpos($this->getSession()->getPage()->getContent(), 'views-query-info'), strpos($this->getSession()->getPage()->getContent(), 'js-view-dom-id'), 'Statistics shown below the preview.');
// Test that the preview title isn't double escaped.
$this->drupalGet("admin/structure/views/nojs/display/test_preview/default/title");
$this->submitForm($edit = ['title' => 'Double & escaped'], 'Apply');
$this->submitForm([], 'Update preview');
$this->assertSession()->elementsCount('xpath', '//div[@id="views-live-preview"]/div[contains(@class, views-query-info)]//td[text()="Double & escaped"]', 1);
}
/**
* Tests the additional information query info area.
*/
public function testPreviewAdditionalInfo(): void {
\Drupal::service('module_installer')->install(['views_ui_test']);
$this->resetAll();
$this->drupalGet('admin/structure/views/view/test_preview/edit');
$this->assertSession()->statusCodeEquals(200);
$this->submitForm($edit = [], 'Update preview');
// Check for implementation of hook_views_preview_info_alter().
// @see views_ui_test.module
// Verify that Views Query Preview Info area was altered.
$this->assertSession()->elementsCount('xpath', '//div[@id="views-live-preview"]/div[contains(@class, views-query-info)]//td[text()="Test row count"]', 1);
// Check that additional assets are attached.
$this->assertStringContainsString('views_ui_test/views_ui_test.test', $this->getDrupalSettings()['ajaxPageState']['libraries'], 'Attached library found.');
$this->assertSession()->responseContains('css/views_ui_test.test.css');
}
/**
* Tests view validation error messages in the preview.
*/
public function testPreviewError(): void {
$this->drupalGet('admin/structure/views/view/test_preview_error/edit');
$this->assertSession()->statusCodeEquals(200);
$this->submitForm($edit = [], 'Update preview');
$this->assertSession()->pageTextContains('Unable to preview due to validation errors.');
}
/**
* Tests HTML is filtered from the view title when previewing.
*/
public function testPreviewTitle(): void {
// Update the view and change title with html tags.
\Drupal::configFactory()->getEditable('views.view.test_preview')
->set('display.default.display_options.title', '<strong>Test preview title</strong>')
->save();
$this->drupalGet('admin/structure/views/view/test_preview/edit');
$this->assertSession()->statusCodeEquals(200);
$this->submitForm([], 'Update preview');
$this->assertSession()->pageTextContains('Test preview title');
// Ensure allowed HTML tags are still displayed.
$this->assertCount(2, $this->xpath('//div[@id="views-live-preview"]//strong[text()=:text]', [':text' => 'Test preview title']));
// Ensure other tags are filtered.
\Drupal::configFactory()->getEditable('views.view.test_preview')
->set('display.default.display_options.title', '<b>Test preview title</b>')
->save();
$this->submitForm([], 'Update preview');
$this->assertCount(0, $this->xpath('//div[@id="views-live-preview"]//b[text()=:text]', [':text' => 'Test preview title']));
}
}

View File

@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\views_ui\Functional;
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 = ['test_view'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@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(): void {
$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->drupalGet($query_settings_path);
$this->submitForm(['query[options][test_setting]' => $random_value], 'Apply');
$this->submitForm([], 'Save');
// Check that the settings are saved into the view itself.
$view = Views::getView('test_view');
$view->initDisplay();
$view->initQuery();
$this->assertEquals($random_value, $view->query->options['test_setting'], 'Query settings got saved');
}
}

View File

@@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\views_ui\Functional;
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 = ['test_view'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* 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 = [];
foreach ($view->displayHandlers->get('default')->getHandlers('field') as $field => $handler) {
$fields[] = $field;
}
return $fields;
}
/**
* Check if the fields are in the correct order.
*
* @param string $view_name
* The name of the view.
* @param array $fields
* Array of field names.
*
* @internal
*/
protected function assertFieldOrder(string $view_name, array $fields): void {
$this->drupalGet('admin/structure/views/nojs/rearrange/' . $view_name . '/default/field');
foreach ($fields as $idx => $field) {
$this->assertSession()->fieldValueEquals('edit-fields-' . $field . '-weight', $idx + 1);
}
}
/**
* Tests field sorting.
*/
public function testRearrangeFields(): void {
$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 = [];
$this->drupalGet('admin/structure/views/nojs/rearrange/' . $view_name . '/default/field');
$this->submitForm($fields, '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 = [];
foreach ($reversedFields as $delta => $field) {
$fields['fields[' . $field . '][weight]'] = $delta;
}
$fields_count = count($fields);
$this->drupalGet('admin/structure/views/nojs/rearrange/' . $view_name . '/default/field');
$this->submitForm($fields, 'Apply');
$this->assertFieldOrder($view_name, $reversedFields);
// Checks that there is a remove link for each field.
$this->assertCount($fields_count, $this->cssSelect('a.views-remove-link'));
}
}

View File

@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\views_ui\Functional;
/**
* Tests the redirecting after saving a views.
*
* @group views_ui
*/
class RedirectTest extends UITestBase {
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = ['test_view', 'test_redirect_view'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests the redirecting.
*/
public function testRedirect(): void {
$view_name = 'test_view';
$random_destination = $this->randomMachineName();
$edit_path = "admin/structure/views/view/$view_name/edit";
// Verify that the user gets redirected to the expected page defined in the
// destination.
$this->drupalGet($edit_path, ['query' => ['destination' => $random_destination]]);
$this->submitForm([], 'Save');
$this->assertSession()->addressEquals($random_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->drupalGet($path_edit_path);
$this->submitForm(['path' => $new_path], 'Apply');
$this->drupalGet($edit_path, ['query' => ['destination' => 'test-redirect-view']]);
$this->submitForm([], 'Save');
// Verify that the user gets redirected to the expected page after changing
// the URL of a page display.
$this->assertSession()->addressEquals($new_path);
}
}

View File

@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\views_ui\Functional;
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}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $modules = ['entity_test'];
/**
* Tests the Views fields report page.
*/
public function testReportFields(): void {
$this->drupalGet('admin/reports/fields/views-fields');
$this->assertSession()->pageTextContains('Used in views');
$this->assertSession()->pageTextContains('No fields have been used in views yet.');
// 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();
// Assert that the newly created field appears in the overview.
$this->drupalGet('admin/reports/fields/views-fields');
$this->assertSession()->responseContains('<td>field_test</td>');
$this->assertSession()->responseContains('>test_field_field_test</a>');
$this->assertSession()->pageTextContains('Used in views');
}
}

View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\views_ui\Functional;
/**
* Tests existence of the views plugin report.
*
* @group views_ui
*/
class ReportTest extends UITestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['views', 'views_ui'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Stores an admin user used by the different tests.
*
* @var \Drupal\user\Entity\User
*/
protected $adminUser;
/**
* Tests the existence of the views plugin report.
*/
public function testReport(): void {
$this->drupalLogin($this->adminUser);
// Test the report page.
$this->drupalGet('admin/reports/views-plugins');
$this->assertSession()->statusCodeEquals(200);
}
}

View File

@@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\views_ui\Functional;
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 = ['test_view'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests changing the row plugin and changing some options of a row.
*/
public function testRowUI(): void {
$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->assertSession()->fieldValueEquals('row[type]', 'fields');
$edit = [
'row[type]' => 'test_row',
];
$this->submitForm($edit, 'Apply');
// Make sure the custom settings form from the test plugin appears.
$this->assertSession()->fieldExists('row_options[test_option]');
$random_name = $this->randomMachineName();
$edit = [
'row_options[test_option]' => $random_name,
];
$this->submitForm($edit, 'Apply');
$this->drupalGet($row_options_url);
// Make sure the custom settings form field has the expected value stored.
$this->assertSession()->fieldValueEquals('row_options[test_option]', $random_name);
$this->drupalGet($view_edit_url);
$this->submitForm([], 'Save');
$this->assertSession()->linkExists('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->assertEquals('test_row', $row['type'], 'Make sure that the test_row got saved as used row plugin.');
$this->assertEquals($random_name, $row['options']['test_option'], 'Make sure that the custom settings field got saved as expected.');
$this->drupalGet($row_plugin_url);
$this->submitForm(['row[type]' => 'fields'], 'Apply');
$this->drupalGet($row_plugin_url);
$this->assertSession()->statusCodeEquals(200);
// Make sure that 'fields' was saved as the row plugin.
$this->assertSession()->fieldValueEquals('row[type]', 'fields');
// 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->submitForm(['row[type]' => 'entity:node'], 'Apply');
$this->assertSession()->addressEquals($row_options_url);
// Make sure the custom settings form from the entity row plugin appears.
$this->assertSession()->fieldValueEquals('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->assertSession()->assertEscaped('Teaser <em>markup</em>');
}
}

View File

@@ -0,0 +1,169 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\views_ui\Functional;
use Drupal\Core\Database\Database;
/**
* 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\Entity\User
*/
protected $adminUser;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp($import_test_views = TRUE, $modules = ['views_test_config']): void {
parent::setUp($import_test_views, $modules);
$this->drupalPlaceBlock('local_tasks_block');
}
/**
* Tests the settings for the edit ui.
*/
public function testEditUI(): void {
$this->drupalLogin($this->adminUser);
// Test the settings tab exists.
$this->drupalGet('admin/structure/views');
$this->assertSession()->linkNotExists('admin/structure/views/settings');
// Test the confirmation message.
$this->drupalGet('admin/structure/views/settings');
$this->submitForm([], 'Save configuration');
$this->assertSession()->pageTextContains('The configuration options have been saved.');
// Configure to always show the default display.
$edit = [
'ui_show_default_display' => TRUE,
];
$this->drupalGet('admin/structure/views/settings');
$this->submitForm($edit, 'Save configuration');
$view = [];
$view['label'] = $this->randomMachineName(16);
$view['id'] = $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->drupalGet('admin/structure/views/add');
$this->submitForm($view, 'Save and edit');
// Configure to not always show the default display.
// If you have a view without a page or block the default display should be
// still shown.
$edit = [
'ui_show_default_display' => FALSE,
];
$this->drupalGet('admin/structure/views/settings');
$this->submitForm($edit, 'Save configuration');
$view['page[create]'] = FALSE;
$this->drupalGet('admin/structure/views/add');
$this->submitForm($view, 'Save and edit');
// Create a view with an additional display, so default should be hidden.
$view['page[create]'] = TRUE;
$view['id'] = $this->randomMachineName();
$this->drupalGet('admin/structure/views/add');
$this->submitForm($view, 'Save and edit');
$this->assertSession()->linkNotExists('Default');
// 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 = [
'ui_show_display_embed' => TRUE,
];
$this->drupalGet('admin/structure/views/settings');
$this->submitForm($edit, 'Save configuration');
$view['id'] = $this->randomMachineName();
$this->drupalGet('admin/structure/views/add');
$this->submitForm($view, 'Save and edit');
$this->assertSession()->buttonExists('edit-displays-top-add-display-embed');
$edit = [
'ui_show_display_embed' => FALSE,
];
$this->drupalGet('admin/structure/views/settings');
$this->submitForm($edit, 'Save configuration');
$this->drupalGet('admin/structure/views/add');
$this->submitForm($view, 'Save and edit');
$this->assertSession()->buttonNotExists('edit-displays-top-add-display-embed');
// Configure to hide/show the sql at the preview.
$edit = [
'ui_show_sql_query_enabled' => FALSE,
];
$this->drupalGet('admin/structure/views/settings');
$this->submitForm($edit, 'Save configuration');
$view['id'] = $this->randomMachineName();
$this->drupalGet('admin/structure/views/add');
$this->submitForm($view, 'Save and edit');
// Verify that the views sql is hidden.
$this->submitForm([], 'Update preview');
$this->assertSession()->elementNotExists('xpath', '//div[@class="views-query-info"]/pre');
$edit = [
'ui_show_sql_query_enabled' => TRUE,
];
$this->drupalGet('admin/structure/views/settings');
$this->submitForm($edit, 'Save configuration');
$view['id'] = $this->randomMachineName();
$this->drupalGet('admin/structure/views/add');
$this->submitForm($view, 'Save and edit');
// Verify that the views sql is shown.
$this->submitForm([], 'Update preview');
$this->assertSession()->elementExists('xpath', '//div[@class="views-query-info"]//pre');
// Verify that no placeholders are shown in the views sql.
$this->assertSession()->elementTextNotContains('xpath', '//div[@class="views-query-info"]//pre', 'db_condition_placeholder');
// Verify that the placeholders in the views sql are replaced by the actual
// values.
$this->assertSession()->elementTextContains('xpath', '//div[@class="views-query-info"]//pre', Database::getConnection()->escapeField("node_field_data.status") . " = '1'");
// Test the advanced settings form.
// Test the confirmation message.
$this->drupalGet('admin/structure/views/settings/advanced');
$this->submitForm([], 'Save configuration');
$this->assertSession()->pageTextContains('The configuration options have been saved.');
$edit = [
'sql_signature' => TRUE,
];
$this->drupalGet('admin/structure/views/settings/advanced');
$this->submitForm($edit, 'Save configuration');
$this->assertSession()->checkboxChecked('edit-sql-signature');
// Test the "Clear Views' cache" button.
$this->drupalGet('admin/structure/views/settings/advanced');
$this->submitForm([], "Clear Views' cache");
$this->assertSession()->pageTextContains('The cache has been cleared.');
}
}

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\views_ui\Functional;
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 = ['test_view'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['views_ui', 'language'];
/**
* Tests changing label, description and tag.
*
* @see views_ui_edit_details_form
*/
public function testDetails(): void {
$view_name = 'test_view';
ConfigurableLanguage::createFromLangcode('fr')->save();
$edit = [
'label' => $this->randomMachineName(),
'tag' => $this->randomMachineName(),
'description' => $this->randomMachineName(30),
'langcode' => 'fr',
];
$this->drupalGet("admin/structure/views/nojs/edit-details/{$view_name}/default");
$this->submitForm($edit, 'Apply');
$this->submitForm([], 'Save');
$view = Views::getView($view_name);
foreach (['label', 'tag', 'description', 'langcode'] as $property) {
$this->assertEquals($edit[$property], $view->storage->get($property), "Make sure the property $property got probably saved.");
}
}
}

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\views_ui\Functional;
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 {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests created a table style view.
*/
public function testWizard(): void {
// Create a new view and check that the first field has a label.
$view = [];
$view['label'] = $this->randomMachineName(16);
$view['id'] = $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->drupalGet('admin/structure/views/add');
$this->submitForm($view, 'Save and edit');
$view = Views::getView($view['id']);
$view->initHandlers();
$this->assertEquals('Title', $view->field['title']->options['label'], 'The field label for table styles is not empty.');
}
}

View File

@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\views_ui\Functional;
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 = ['test_view'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests changing the style plugin and changing some options of a style.
*/
public function testStyleUI(): void {
$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->assertSession()->fieldValueEquals('style[type]', 'default');
$edit = [
'style[type]' => 'test_style',
];
$this->submitForm($edit, 'Apply');
$this->assertSession()->fieldExists('style_options[test_option]');
$random_name = $this->randomMachineName();
$edit = [
'style_options[test_option]' => $random_name,
];
$this->submitForm($edit, 'Apply');
$this->drupalGet($style_options_url);
$this->assertSession()->fieldValueEquals('style_options[test_option]', $random_name);
$this->drupalGet($view_edit_url);
$this->submitForm([], 'Save');
$this->assertSession()->linkExists('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->assertEquals('test_style', $style['type'], 'Make sure that the test_style got saved as used style plugin.');
$this->assertEquals($random_name, $style['options']['test_option'], '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->drupalGet("admin/structure/views/view/{$view_name}/edit");
$this->submitForm([], 'Add Page');
$this->drupalGet("admin/structure/views/nojs/display/{$view_name}/page_1/row");
$this->submitForm(['row[type]' => 'fields'], 'Apply');
// If fields are being used this text will not be shown.
$this->assertSession()->pageTextNotContains('The selected style or row format does not use fields.');
}
}

View File

@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\views_ui\Functional;
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}
*/
protected static $modules = ['entity_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests that the right tokens are shown as available for replacement.
*/
public function testTokenUI(): void {
$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->drupalGet("admin/structure/views/nojs/add-handler/{$id}/page_1/argument");
$this->submitForm(['name[views.null]' => 1], 'Add and configure contextual filters');
$this->submitForm([], 'Apply');
$this->drupalGet("admin/structure/views/nojs/add-handler/{$id}/page_1/header");
$this->submitForm(['name[views.area]' => 'views.area'], 'Add and configure header');
// Test that field tokens are shown.
$this->assertSession()->pageTextContains('{{ title }} == Content: Title');
// Test that argument tokens are shown.
$this->assertSession()->pageTextContains('{{ arguments.null }} == Global: Null title');
}
}

View File

@@ -0,0 +1,131 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\views_ui\Functional;
use Drupal\language\Entity\ConfigurableLanguage;
// cspell:ignore fichiers
/**
* Tests that translated strings in views UI don't override original strings.
*
* @group views_ui
*/
class TranslatedViewTest extends UITestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = [
'config_translation',
'views_ui',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Languages to enable.
*
* @var array
*/
protected $langcodes = [
'fr',
];
/**
* Administrator user for tests.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* {@inheritdoc}
*/
protected function setUp($import_test_views = TRUE, $modules = []): void {
parent::setUp($import_test_views, $modules);
$permissions = [
'administer site configuration',
'administer views',
'translate configuration',
'translate interface',
];
$this->drupalPlaceBlock('local_tasks_block', ['id' => 'test_role_admin_test_local_tasks_block']);
// 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(): void {
$translation_url = 'admin/structure/views/view/files/translate/fr/add';
$edit_url = 'admin/structure/views/view/files';
// Check the original string.
$this->drupalGet($edit_url);
$this->assertSession()->titleEquals('Files (File) | Drupal');
// Translate the label of the view.
$this->drupalGet($translation_url);
$edit = [
'translation[config_names][views.view.files][label]' => 'Fichiers',
];
$this->submitForm($edit, 'Save translation');
// Check if the label is translated.
$this->drupalGet($edit_url, ['language' => \Drupal::languageManager()->getLanguage('fr')]);
$this->assertSession()->titleEquals('Files (File) | Drupal');
$this->assertSession()->pageTextNotContains('Fichiers');
// Ensure that "Link URL" and "Link Path" fields are translatable.
// First, Add the block display and change pager's 'link display' to
// custom URL.
// Second, change filename to use plain text and rewrite output with link.
$this->drupalGet($edit_url);
$this->submitForm([], 'Add Block');
$this->drupalGet('admin/structure/views/nojs/display/files/block_1/link_display');
$edit = [
'link_display' => 'custom_url',
'link_url' => '/node',
];
$this->submitForm($edit, 'Apply');
$this->submitForm([], 'Save');
$this->drupalGet('admin/structure/views/nojs/handler/files/block_1/field/filename');
$edit = [
'override[dropdown]' => 'block_1',
'options[type]' => 'string',
'options[alter][path]' => '/node',
'options[alter][make_link]' => 1,
];
$this->submitForm($edit, 'Apply');
$this->submitForm([], 'Save');
// Visit the translation page and ensure that field exists.
$this->drupalGet($translation_url);
$this->assertSession()->fieldExists('translation[config_names][views.view.files][display][block_1][display_options][fields][filename][alter][path]');
$this->assertSession()->fieldExists('translation[config_names][views.view.files][display][default][display_options][link_url]');
// Assert that the View translation link is shown when viewing a display.
$this->drupalGet($edit_url);
$this->assertSession()->linkExists('Translate view');
$this->drupalGet('/admin/structure/views/view/files/edit/block_1');
$this->assertSession()->linkExists('Translate view');
}
}

View File

@@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\views_ui\Functional;
use Drupal\Tests\views\Functional\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
*/
protected static $modules = ['node', 'views_ui', 'block', 'taxonomy'];
/**
* {@inheritdoc}
*/
protected function setUp($import_test_views = TRUE, $modules = ['views_test_config']): void {
parent::setUp($import_test_views, $modules);
$this->enableViewsTestModule();
$this->adminUser = $this->drupalCreateUser(['administer views']);
$this->fullAdminUser = $this->drupalCreateUser(['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 = []) {
// Create a new view in the UI.
$default = [];
$default['label'] = $this->randomMachineName(16);
$default['id'] = $this->randomMachineName(16);
$default['description'] = $this->randomMachineName(16);
$default['page[create]'] = TRUE;
$default['page[path]'] = $default['id'];
$view += $default;
$this->drupalGet('admin/structure/views/add');
$this->submitForm($view, 'Save and edit');
return $default;
}
/**
* {@inheritdoc}
*/
protected function drupalGet($path, array $options = [], array $headers = []) {
$url = $this->buildUrl($path, $options);
// Ensure that each nojs page is accessible via ajax as well.
if (str_contains($url, '/nojs/')) {
$url = preg_replace('|/nojs/|', '/ajax/', $url, 1);
$result = $this->drupalGet($url, $options);
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->responseHeaderEquals('Content-Type', 'application/json');
$this->assertNotEmpty(json_decode($result), 'Ensure that the AJAX request returned valid content.');
}
return parent::drupalGet($path, $options, $headers);
}
}

View File

@@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\views_ui\Functional;
/**
* Tests covering Preview of unsaved Views.
*
* @group views_ui
*/
class UnsavedPreviewTest extends UITestBase {
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = ['content'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* An admin user with the 'administer views' permission.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* {@inheritdoc}
*/
protected static $modules = ['node', 'views_ui'];
/**
* Sets up a Drupal site for running functional and integration tests.
*/
protected function setUp($import_test_views = TRUE, $modules = ['views_test_config']): void {
parent::setUp(FALSE, $modules);
$this->adminUser = $this->drupalCreateUser(['administer views']);
$this->drupalLogin($this->adminUser);
}
/**
* Tests previews of unsaved new page displays.
*/
public function testUnsavedPageDisplayPreview(): void {
$this->drupalCreateContentType(['type' => 'page']);
for ($i = 0; $i < 5; $i++) {
$this->drupalCreateNode();
}
$this->drupalGet('admin/structure/views/view/content');
$this->assertSession()->statusCodeEquals(200);
$this->submitForm([], 'Add Page');
$this->assertSession()->statusCodeEquals(200);
$this->drupalGet('admin/structure/views/nojs/display/content/page_2/path');
$this->assertSession()->statusCodeEquals(200);
$this->submitForm(['path' => 'foobar'], 'Apply');
$this->assertSession()->statusCodeEquals(200);
$this->submitForm([], 'Update preview');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains('This display has no path');
$this->drupalGet('admin/structure/views/view/content/edit/page_2');
$this->assertSession()->statusCodeEquals(200);
$this->submitForm([], 'Save');
$this->assertSession()->statusCodeEquals(200);
$this->submitForm([], 'Update preview');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->linkByHrefExists('foobar');
}
}

View File

@@ -0,0 +1,276 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\views_ui\Functional;
use Drupal\Core\Database\Database;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\views\Entity\View;
/**
* Tests some general functionality of editing views, like deleting a view.
*
* @group views_ui
* @group #slow
*/
class ViewEditTest extends UITestBase {
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = ['test_view', 'test_display', 'test_groupwise_term_ui'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests the delete link on a views UI.
*/
public function testDeleteLink(): void {
$this->drupalGet('admin/structure/views/view/test_view');
$this->assertSession()->linkExists('Delete view', 0, 'Ensure that the view delete link appears');
$view = $this->container->get('entity_type.manager')->getStorage('view')->load('test_view');
$this->assertInstanceOf(View::class, $view);
$this->clickLink('Delete view');
$this->assertSession()->addressEquals('admin/structure/views/view/test_view/delete');
$this->submitForm([], 'Delete');
$this->assertSession()->pageTextContains("The view {$view->label()} has been deleted.");
$this->assertSession()->addressEquals('admin/structure/views');
$view = $this->container->get('entity_type.manager')->getStorage('view')->load('test_view');
$this->assertNotInstanceOf(View::class, $view);
}
/**
* Tests the machine name and administrative comment forms.
*/
public function testOtherOptions(): void {
\Drupal::service('module_installer')->install(['dblog']);
$this->drupalGet('admin/structure/views/view/test_view');
// Add a new attachment display.
$this->submitForm([], 'Add Attachment');
// Test that a long administrative comment is truncated.
$edit = ['display_comment' => 'one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen'];
$this->drupalGet('admin/structure/views/nojs/display/test_view/attachment_1/display_comment');
$this->submitForm($edit, 'Apply');
$this->assertSession()->pageTextContains('one two three four five six seven eight nine ten eleven twelve thirteen…');
// Change the machine name for the display from page_1 to test_1.
$edit = ['display_id' => 'test_1'];
$this->drupalGet('admin/structure/views/nojs/display/test_view/attachment_1/display_id');
$this->submitForm($edit, 'Apply');
$this->assertSession()->linkExists('test_1');
// Save the view, and test the new ID has been saved.
$this->submitForm([], 'Save');
$view = \Drupal::entityTypeManager()->getStorage('view')->load('test_view');
$displays = $view->get('display');
$this->assertNotEmpty($displays['test_1'], 'Display data found for new display ID key.');
$this->assertSame('test_1', $displays['test_1']['id'], 'New display ID matches the display ID key.');
$this->assertArrayNotHasKey('attachment_1', $displays);
// Set to the same machine name and save the View.
$edit = ['display_id' => 'test_1'];
$this->drupalGet('admin/structure/views/nojs/display/test_view/test_1/display_id');
$this->submitForm($edit, 'Apply');
$this->submitForm([], 'Save');
$this->assertSession()->linkExists('test_1');
// Test the form validation with invalid IDs.
$machine_name_edit_url = 'admin/structure/views/nojs/display/test_view/test_1/display_id';
$error_text = 'Display machine name must contain only lowercase letters, numbers, or underscores.';
// Test that potential invalid display ID requests are detected
$this->drupalGet('admin/structure/views/ajax/handler/test_view/fake_display_name/filter/title');
$arguments = [
'@display_id' => 'fake_display_name',
];
$logged = Database::getConnection()->select('watchdog')
->fields('watchdog', ['variables'])
->condition('type', 'views')
->condition('message', 'setDisplay() called with invalid display ID "@display_id".')
->execute()
->fetchField();
$this->assertEquals(serialize($arguments), $logged);
$edit = ['display_id' => 'test 1'];
$this->drupalGet($machine_name_edit_url);
$this->submitForm($edit, 'Apply');
$this->assertSession()->pageTextContains($error_text);
$edit = ['display_id' => 'test_1#'];
$this->drupalGet($machine_name_edit_url);
$this->submitForm($edit, 'Apply');
$this->assertSession()->pageTextContains($error_text);
// Test using an existing display ID.
$edit = ['display_id' => 'default'];
$this->drupalGet($machine_name_edit_url);
$this->submitForm($edit, 'Apply');
$this->assertSession()->pageTextContains('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->assertSession()->linkExists('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->drupalGet('admin/structure/views/nojs/rearrange/test_view/default/field');
$this->submitForm($fields, 'Apply');
$this->submitForm([], 'Save');
$this->submitForm([], 'Cancel');
// Verify that no error message is displayed.
$this->assertSession()->elementNotExists('xpath', '//div[contains(@class, "error")]');
// Verify page was redirected to the view listing.
$this->assertSession()->addressEquals('admin/structure/views');
}
/**
* Tests the language options on the views edit form.
*/
public function testEditFormLanguageOptions(): void {
$assert_session = $this->assertSession();
// Language options should not exist without language module.
$test_views = [
'test_view' => 'default',
'test_display' => 'page_1',
];
foreach ($test_views as $view_name => $display) {
$this->drupalGet('admin/structure/views/view/' . $view_name);
$this->assertSession()->statusCodeEquals(200);
$langcode_url = 'admin/structure/views/nojs/display/' . $view_name . '/' . $display . '/rendering_language';
$this->assertSession()->linkByHrefNotExists($langcode_url);
$assert_session->linkNotExistsExact('Content language selected for page');
$this->assertSession()->linkNotExists('Content language of view row');
}
// Make the site multilingual and test the options again.
$this->container->get('module_installer')->install(['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->assertSession()->statusCodeEquals(200);
$langcode_url = 'admin/structure/views/nojs/display/' . $view_name . '/' . $display . '/rendering_language';
if ($view_name == 'test_view') {
$this->assertSession()->linkByHrefNotExists($langcode_url);
$assert_session->linkNotExistsExact('Content language selected for page');
$this->assertSession()->linkNotExists('Content language of view row');
}
else {
$this->assertSession()->linkByHrefExists($langcode_url);
$assert_session->linkNotExistsExact('Content language selected for page');
$this->assertSession()->linkExists('Content language of view row');
}
$this->drupalGet($langcode_url);
$this->assertSession()->statusCodeEquals(200);
if ($view_name == 'test_view') {
$this->assertSession()->pageTextContains('The view is not based on a translatable entity type or the site is not multilingual.');
}
else {
$this->assertSession()->fieldValueEquals('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 = [
'***LANGUAGE_entity_translation***',
'***LANGUAGE_entity_default***',
'***LANGUAGE_site_default***',
'***LANGUAGE_language_interface***',
'en',
'hu',
];
$elements = $this->assertSession()->selectExists('edit-rendering-language')->findAll('css', 'option');
$elements = array_map(function ($element) {
return $element->getValue();
}, $elements);
$this->assertSame($expected_elements, $elements);
// 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->drupalGet('/admin/people/permissions');
$this->submitForm($edit, 'Save permissions');
// Enable Content language negotiation so we have one more item
// to select.
$edit = [
'language_content[configurable]' => TRUE,
];
$this->drupalGet('admin/config/regional/language/detection');
$this->submitForm($edit, 'Save settings');
// Choose the new negotiation as the rendering language.
$edit = [
'rendering_language' => '***LANGUAGE_language_content***',
];
$this->drupalGet('/admin/structure/views/nojs/display/' . $view_name . '/' . $display . '/rendering_language');
$this->submitForm($edit, 'Apply');
// Disable language content negotiation.
$edit = [
'language_content[configurable]' => FALSE,
];
$this->drupalGet('admin/config/regional/language/detection');
$this->submitForm($edit, 'Save settings');
// Check that the previous selection is listed and selected.
$this->drupalGet($langcode_url);
$this->assertTrue($this->assertSession()->optionExists('edit-rendering-language', '***LANGUAGE_language_content***')->isSelected());
// Check the order for the langcode filter.
$langcode_url = 'admin/structure/views/nojs/handler/' . $view_name . '/' . $display . '/filter/langcode';
$this->drupalGet($langcode_url);
$this->assertSession()->statusCodeEquals(200);
$expected_elements = [
'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->assertEquals($expected_elements[$i], $elements[$i]->getAttribute('value'));
}
}
}
}
/**
* Tests Representative Node for a Taxonomy Term.
*/
public function testRelationRepresentativeNode(): void {
// Populate and submit the form.
$edit["name[taxonomy_term_field_data.tid_representative]"] = TRUE;
$this->drupalGet('admin/structure/views/nojs/add-handler/test_groupwise_term_ui/default/relationship');
$this->submitForm($edit, 'Add and configure relationships');
// Apply changes.
$edit = [];
$this->drupalGet('admin/structure/views/nojs/handler/test_groupwise_term_ui/default/relationship/tid_representative');
$this->submitForm($edit, 'Apply');
}
}

View File

@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\views_ui\Functional;
use Drupal\views\Entity\View;
use Drupal\views\Views;
/**
* Tests the views list.
*
* @group views_ui
*/
class ViewsListTest extends UITestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['block', 'views_ui'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* A user with permission to administer views.
*
* @var \Drupal\user\Entity\User
*/
protected $adminUser;
/**
* {@inheritdoc}
*/
protected function setUp($import_test_views = TRUE, $modules = []): void {
parent::setUp($import_test_views, $modules);
$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(): void {
// Check if we can access the main views admin page.
$this->drupalGet('admin/structure/views');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->linkExists('Add view');
// Check that there is a link to the content view without a destination
// parameter.
$this->drupalGet('admin/structure/views');
$links = $this->getSession()->getPage()->findAll('xpath', "//a[contains(@href, 'admin/structure/views/view/content')]");
$this->assertStringEndsWith('admin/structure/views/view/content', $links[0]->getAttribute('href'));
$this->assertSession()->linkByHrefExists('admin/structure/views/view/content/delete?destination');
// 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'] = $values['label'] = '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->assertSession()->elementsCount('xpath', '//tbody/tr[contains(@class,"views-ui-list-enabled")]', $limit);
}
}

View File

@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\views_ui\Functional;
use Drupal\Component\Utility\Unicode;
use Drupal\Tests\views\Functional\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 {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests filling in the wizard with really long strings.
*/
public function testWizardFieldLength(): void {
$view = [];
$view['label'] = $this->randomMachineName(256);
$view['id'] = $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->drupalGet('admin/structure/views/add');
$this->submitForm($view, 'Save and edit');
$this->assertSession()->pageTextContains('Machine-readable name cannot be longer than 128 characters but is currently 129 characters long.');
$this->assertSession()->pageTextContains('Path cannot be longer than 254 characters but is currently 255 characters long.');
$this->assertSession()->pageTextContains('Page title cannot be longer than 255 characters but is currently 256 characters long.');
$this->assertSession()->pageTextContains('View name cannot be longer than 255 characters but is currently 256 characters long.');
$this->assertSession()->pageTextContains('Feed path cannot be longer than 254 characters but is currently 255 characters long.');
$this->assertSession()->pageTextContains('Block title cannot be longer than 255 characters but is currently 256 characters long.');
$this->assertSession()->pageTextContains('REST export path cannot be longer than 254 characters but is currently 255 characters long.');
$view['label'] = $this->randomMachineName(255);
$view['id'] = $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);
// Make sure the view saving was successful and the browser got redirected
// to the edit page.
$this->drupalGet('admin/structure/views/add');
$this->submitForm($view, 'Save and edit');
$this->assertSession()->addressEquals('admin/structure/views/view/' . $view['id']);
// Assert that the page title is correctly truncated.
$this->assertSession()->pageTextContains(Unicode::truncate($view['page[title]'], 32, FALSE, TRUE));
}
}

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\views_ui\Functional;
/**
* Tests the Xss vulnerability.
*
* @group views_ui
*/
class XssTest extends UITestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['node', 'user', 'views_ui', 'views_ui_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
public function testViewsUi(): void {
$this->drupalGet('admin/structure/views/view/sa_contrib_2013_035');
// Verify that the field admin label is properly escaped.
$this->assertSession()->assertEscaped('<marquee>test</marquee>');
$this->drupalGet('admin/structure/views/nojs/handler/sa_contrib_2013_035/page_1/header/area');
// Verify that the token label is properly escaped.
$this->assertSession()->assertEscaped('{{ title }} == <marquee>test</marquee>');
$this->assertSession()->assertEscaped('{{ title_1 }} == <script>alert("XSS")</script>');
}
/**
* Checks the admin UI for double escaping.
*/
public function testNoDoubleEscaping(): void {
$this->drupalGet('admin/structure/views');
$this->assertSession()->assertNoEscaped('&lt;');
$this->drupalGet('admin/structure/views/view/sa_contrib_2013_035');
$this->assertSession()->assertNoEscaped('&lt;');
$this->drupalGet('admin/structure/views/nojs/handler/sa_contrib_2013_035/page_1/header/area');
$this->assertSession()->assertNoEscaped('&lt;');
}
}

View File

@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\views_ui\FunctionalJavascript;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Tests the admin UI AJAX interactions.
*
* @group views_ui
*/
class AdminAjaxTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'views_ui',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'views_ui_test_theme';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalLogin($this->createUser([
'administer views',
]));
}
/**
* Confirms that form_alter is triggered after AJAX rebuilds.
*/
public function testAjaxRebuild(): void {
\Drupal::service('theme_installer')->install(['views_ui_test_theme']);
$this->config('system.theme')
->set('default', 'views_ui_test_theme')
->save();
$page = $this->getSession()->getPage();
$assert_session = $this->assertSession();
$this->drupalGet('admin/structure/views/view/user_admin_people');
$assert_session->pageTextContains('This is text added to the display tabs at the top');
$assert_session->pageTextContains('This is text added to the display edit form');
$page->clickLink('User: Name (Username)');
$assert_session->waitForElementVisible('css', '.views-ui-dialog');
$page->fillField('Label', 'New Title');
$page->find('css', '.ui-dialog-buttonset button:contains("Apply")')->press();
$assert_session->waitForElementRemoved('css', '.views-ui-dialog');
$assert_session->pageTextContains('This is text added to the display tabs at the top');
$assert_session->pageTextContains('This is text added to the display edit form');
}
/**
* Tests body scroll.
*/
public function testBodyScroll(): void {
$this->drupalGet('admin/structure/views/view/user_admin_people');
$page = $this->getSession()->getPage();
foreach (['name[views.nothing]', 'name[views.dropbutton]'] as $field) {
$page->find('css', '#views-add-field')->click();
$this->assertSession()->assertWaitOnAjaxRequest();
$page->checkField($field);
$page->find('css', '.ui-dialog-buttonset')->pressButton('Add and configure fields');
$this->assertSession()->assertWaitOnAjaxRequest();
$this->assertJsCondition('document.documentElement.style.overflow === "hidden"');
$page->find('css', '.ui-dialog-buttonset')->pressButton('Apply');
$this->assertSession()->assertWaitOnAjaxRequest();
// Check overflow.
$this->assertJsCondition('document.documentElement.style.overflow !== "hidden"');
}
}
}

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