Initial Drupal 11 with DDEV setup

This commit is contained in:
gluebox
2025-10-08 11:39:17 -04:00
commit 89ef74b305
25344 changed files with 2599172 additions and 0 deletions

View File

@ -0,0 +1,45 @@
display_extenders: { }
sql_signature: false
ui:
show:
additional_queries: false
advanced_column: false
default_display: false
performance_statistics: false
preview_information: true
sql_query:
enabled: false
where: above
display_embed: false
always_live_preview: true
exposed_filter_any_label: old_any
field_rewrite_elements:
div: DIV
span: SPAN
h1: H1
h2: H2
h3: H3
h4: H4
h5: H5
h6: H6
p: P
header: HEADER
footer: FOOTER
article: ARTICLE
section: SECTION
aside: ASIDE
details: DETAILS
blockquote: BLOCKQUOTE
figure: FIGURE
address: ADDRESS
code: CODE
pre: PRE
var: VAR
samp: SAMP
kbd: KBD
strong: STRONG
em: EM
del: DEL
ins: INS
q: Q
s: S

View File

@ -0,0 +1,5 @@
# Schema for the views access plugins.
views.access.none:
type: mapping
label: 'None'

View File

@ -0,0 +1,90 @@
# Schema for the views area plugins.
views.area.*:
type: views_area
label: 'Default area'
views.area.entity:
type: views_area
label: 'Entity'
mapping:
target:
type: string
label: 'The target entity'
view_mode:
type: string
label: 'View mode'
tokenize:
type: boolean
label: 'Use replacement tokens from the first row'
bypass_access:
type: boolean
label: 'Bypass access checks'
views.area.text:
type: views_area
label: 'Text'
mapping:
content:
type: text_format
label: 'The formatted text of the area'
tokenize:
type: boolean
label: 'Use replacement tokens from the first row'
views.area.text_custom:
type: views_area
label: 'Text custom'
mapping:
content:
type: text
label: 'The shown text of the area'
tokenize:
type: boolean
label: 'Use replacement tokens from the first row'
views.area.result:
type: views_area
label: 'Result'
mapping:
content:
type: text
label: 'The shown text of the result summary area'
views.area.title:
type: views_area
label: 'Title'
mapping:
title:
type: label
label: 'The title which will be overridden for the page'
views.area.view:
type: views_area
label: 'View'
mapping:
view_to_insert:
type: string
label: 'View to insert'
inherit_arguments:
type: boolean
label: 'Inherit contextual filters'
views.area.http_status_code:
type: views_area
label: 'HTTP status code'
mapping:
status_code:
type: integer
label: 'HTTP status code'
views.area.display_link:
type: views_area
label: 'Display link'
mapping:
display_id:
type: string
label: 'The display ID of the view display to link to.'
label:
type: label
label: 'The label of the link.'

View File

@ -0,0 +1,163 @@
# Schema for the views argument plugins.
views.argument.*:
type: views_argument
label: 'Default argument'
views.argument.many_to_one:
type: views_argument
label: 'Many to one'
mapping:
break_phrase:
type: boolean
label: 'Allow multiple values'
add_table:
type: boolean
label: 'Allow multiple filter values to work together'
require_value:
type: boolean
label: 'Do not display items with no value in summary'
reduce_duplicates:
type: boolean
label: 'Reduce duplicates'
views.argument.null:
type: views_argument
label: 'Null'
mapping:
must_not_be:
type: boolean
label: 'Fail basic validation if any argument is given'
views.argument.numeric:
type: views_argument
label: 'Numeric'
mapping:
break_phrase:
type: boolean
label: 'Allow multiple values'
not:
type: boolean
label: 'Exclude'
views.argument.entity_id:
type: views.argument.numeric
label: 'Entity ID'
views.argument.entity_target_id:
type: views.argument.numeric
label: 'Entity Target ID'
mapping:
target_entity_type_id:
type: string
label: 'Target entity type ID'
views.argument.string:
type: views_argument
label: 'String'
mapping:
glossary:
type: boolean
label: 'Glossary mode'
limit:
type: integer
label: 'Character limit'
case:
type: string
label: 'Case'
path_case:
type: string
label: 'Case in path'
transform_dash:
type: boolean
label: 'Transform spaces to dashes in URL'
break_phrase:
type: boolean
label: 'Allow multiple values'
add_table:
type: boolean
label: 'Allow multiple filter values to work together'
require_value:
type: boolean
label: 'Do not display items with no value in summary'
views.argument.broken:
type: views_argument
label: 'Broken'
views.argument.date:
type: views_argument
label: 'Date'
mapping:
date:
type: string
label: 'Date'
node_created:
type: string
label: 'Node Creation Time'
node_changed:
type: string
label: 'Node Update Time'
views.argument.date_day:
type: views.argument.date
label: 'Day Date'
mapping:
day:
type: string
label: 'Day'
views.argument.formula:
type: views_argument
label: 'Formula'
mapping:
placeholder:
type: string
label: 'Place Holder'
formula:
type: string
label: 'Formula Used'
views.argument.date_fulldate:
type: views.argument.date
label: 'Full Date'
mapping:
created:
type: string
label: 'Full Date'
views.argument.groupby_numeric:
type: views_argument
label: 'Group by Numeric'
views.argument.date_month:
type: views.argument.date
label: 'Month Date'
mapping:
month:
type: string
label: 'Month'
views.argument.standard:
type: views_argument
label: 'Standard'
views.argument.date_week:
type: views.argument.date
label: 'Week Date'
views.argument.date_year:
type: views.argument.date
label: 'Year Date'
views.argument.date_year_month:
type: views.argument.date
label: 'YearMonthDate'
mapping:
created:
type: string
label: 'Date Year month'
views.argument.language:
type: views_argument
label: 'Language'

View File

@ -0,0 +1,38 @@
# Schema for the views default arguments.
views.argument_default.*:
type: mapping
label: 'Base default argument'
views.argument_default.fixed:
type: mapping
label: 'Fixed'
mapping:
argument:
type: string
label: 'Fixed value'
views.argument_default.raw:
type: mapping
label: 'Raw value from URL'
mapping:
index:
type: integer
label: 'Path component'
use_alias:
type: boolean
label: 'Use path alias'
views.argument_default.query_parameter:
type: mapping
label: 'Query parameter'
mapping:
query_param:
type: string
label: 'Parameter'
fallback:
type: string
label: 'Fallback value'
multiple:
type: string
label: 'Multiple values'

View File

@ -0,0 +1,41 @@
# Schema for the views argument validators.
views.argument_validator.none:
type: sequence
label: 'Basic validation'
sequence:
type: string
views.argument_validator.php:
type: mapping
label: 'PHP Code'
mapping:
code:
type: string
label: 'PHP validate code'
views.argument_validator.*:
type: mapping
label: 'Default argument validator'
views.argument_validator_entity:
type: mapping
mapping:
bundles:
type: sequence
label: 'Bundles'
sequence:
type: string
label: 'Bundle'
access:
type: boolean
label: 'Access'
operation:
type: string
label: 'Access operation to check'
multiple:
type: integer
label: 'Multiple arguments'
views.argument_validator.entity:*:
type: views.argument_validator_entity

View File

@ -0,0 +1,38 @@
# Schema for the views cache.
views.cache.none:
type: views_cache
label: 'No caching'
mapping:
options:
type: sequence
label: 'Options'
views.cache.tag:
type: views_cache
label: 'Tag based caching'
mapping:
options:
type: sequence
label: 'Options'
views.cache.time:
type: views_cache
label: 'Time based caching'
mapping:
options:
type: mapping
label: 'Cache options'
mapping:
results_lifespan:
type: integer
label: 'The length of time raw query results should be cached.'
results_lifespan_custom:
type: integer
label: 'Length of time in seconds raw query results should be cached.'
output_lifespan:
type: integer
label: 'The length of time rendered HTML output should be cached.'
output_lifespan_custom:
type: integer
label: 'Length of time in seconds rendered HTML output should be cached.'

View File

@ -0,0 +1,923 @@
# Basic data types for views.
views_display:
type: mapping
label: 'Display options'
mapping:
enabled:
type: boolean
label: 'Status'
title:
type: text
label: 'Display title'
format:
type: string
label: 'Format'
fields:
type: sequence
label: 'Fields'
sequence:
type: views.field.[plugin_id]
pager:
type: mapping
label: 'Pager'
mapping:
type:
type: string
label: 'Pager type'
constraints:
PluginExists:
manager: plugin.manager.views.pager
interface: 'Drupal\views\Plugin\views\pager\PagerPluginBase'
options:
type: views.pager.[%parent.type]
exposed_form:
type: mapping
label: 'Exposed form'
mapping:
type:
type: string
label: 'Exposed form type'
constraints:
PluginExists:
manager: plugin.manager.views.exposed_form
interface: 'Drupal\views\Plugin\views\exposed_form\ExposedFormPluginInterface'
options:
label: 'Options'
type: views.exposed_form.[%parent.type]
access:
type: mapping
label: 'Access'
mapping:
type:
type: string
label: 'Access type'
constraints:
PluginExists:
manager: plugin.manager.views.access
options:
type: views.access.[%parent.type]
cache:
type: views.cache.[type]
empty:
type: sequence
label: 'No results behavior'
sequence:
type: views.area.[plugin_id]
sorts:
type: sequence
label: 'Sorts'
sequence:
type: views.sort.[plugin_id]
arguments:
type: sequence
label: 'Arguments'
sequence:
type: views.argument.[plugin_id]
filters:
type: sequence
label: 'Filters'
sequence:
type: views.filter.[plugin_id]
filter_groups:
type: mapping
label: 'Groups'
mapping:
operator:
type: string
label: 'Operator'
groups:
type: sequence
label: 'Groups'
sequence:
type: string
label: 'Operator'
style:
type: mapping
label: 'Format'
mapping:
type:
type: string
label: 'Type'
constraints:
PluginExists:
manager: plugin.manager.views.style
interface: 'Drupal\views\Plugin\views\style\StylePluginBase'
options:
type: views.style.[%parent.type]
row:
type: mapping
label: 'Row'
mapping:
type:
type: string
label: 'Row type'
constraints:
PluginExists:
manager: plugin.manager.views.row
options:
type: views.row.[%parent.type]
query:
type: mapping
label: 'Query'
mapping:
type:
type: string
label: 'Query type'
constraints:
PluginExists:
manager: plugin.manager.views.query
options:
type: views.query.[%parent.type]
defaults:
type: mapping
label: 'Defaults'
mapping:
empty:
type: boolean
label: 'Empty'
access:
type: boolean
label: 'Access restrictions'
cache:
type: boolean
label: 'Caching'
query:
type: boolean
label: 'Query options'
title:
type: boolean
label: 'Title'
css_class:
type: boolean
label: 'CSS class'
display_description:
type: boolean
label: 'Administrative description'
use_ajax:
type: boolean
label: 'Use AJAX'
hide_attachment_summary:
type: boolean
label: 'Hide attachments when displaying a contextual filter summary'
show_admin_links:
type: boolean
label: 'Show contextual links'
pager:
type: boolean
label: 'Use pager'
use_more:
type: boolean
label: 'Create more link'
use_more_always:
type: boolean
label: 'Display ''more'' link only if there is more content'
use_more_text:
type: boolean
label: 'The text to display for the more link.'
exposed_form:
type: boolean
label: 'Exposed form style'
link_display:
type: boolean
label: 'Link display'
link_url:
type: boolean
label: 'Link URL'
group_by:
type: boolean
label: 'Aggregate'
style:
type: boolean
label: 'Style'
row:
type: boolean
label: 'Row'
relationships:
type: boolean
label: 'Relationships'
fields:
type: boolean
label: 'Fields'
sorts:
type: boolean
label: 'Sorts'
arguments:
type: boolean
label: 'Arguments'
filters:
type: boolean
label: 'Filters'
filter_groups:
type: boolean
label: 'Filter groups'
header:
type: boolean
label: 'Header'
footer:
type: boolean
label: 'Footer'
relationships:
type: sequence
label: 'Relationships'
sequence:
type: views.relationship.[plugin_id]
css_class:
type: string
label: 'CSS class'
use_ajax:
type: boolean
label: 'Use AJAX'
group_by:
type: boolean
label: 'Aggregate'
display_description:
type: label
label: 'Administrative description'
show_admin_links:
type: boolean
label: 'Show contextual links'
use_more:
type: boolean
label: 'Create more link'
use_more_always:
type: boolean
label: 'Display ''more'' link only if there is more content'
use_more_text:
type: label
label: 'The text to display for the more link.'
link_display:
type: string
label: 'Link display'
link_url:
type: text
label: 'Link URL'
header:
type: sequence
label: 'Header'
sequence:
type: views.area.[plugin_id]
footer:
type: sequence
label: 'Footer'
sequence:
type: views.area.[plugin_id]
display_comment:
type: label
label: 'Display comment'
hide_attachment_summary:
type: boolean
label: 'Hide attachments in summary'
rendering_language:
type: string
label: 'Entity language'
exposed_block:
type: boolean
label: 'Put the exposed form in a block'
display_extenders:
type: sequence
label: 'Display extenders'
sequence:
type: views.display_extender.[%key]
views_sort:
type: views_handler
label: 'Sort criteria'
mapping:
order:
type: string
label: 'Sort order'
expose:
type: views.sort_expose.[%parent.plugin_id]
exposed:
type: boolean
label: 'Expose this sort to visitors, to allow them to change it'
plugin_id:
type: string
label: 'Plugin ID'
constraints:
PluginExists:
manager: plugin.manager.views.sort
# @todo Remove this line and fix all views in core which use invalid
# sort plugins in https://drupal.org/i/3387325.
allowFallback: true
views_sort_expose:
type: mapping
mapping:
label:
type: label
label: 'Label'
field_identifier:
type: string
label: 'Field identifier'
views_area:
type: views_handler
label: 'Area'
mapping:
label:
type: label
label: 'A string to identify the area instance in the admin UI.'
empty:
type: boolean
label: 'Display even if view has no result'
plugin_id:
type: string
label: 'Plugin ID'
constraints:
PluginExists:
manager: plugin.manager.views.area
# @todo Remove this line and fix all views in core which use invalid
# area plugins in https://drupal.org/i/3387325.
allowFallback: true
views_handler:
type: mapping
mapping:
id:
type: string
label: 'A unique ID per handler type'
table:
type: string
label: 'The views_data table for this handler'
field:
type: string
label: 'The views_data field for this handler'
relationship:
type: string
label: 'The ID of the relationship instance used by this handler'
group_type:
type: string
label: 'A sql aggregation type'
admin_label:
type: label
label: 'A string to identify the handler instance in the admin UI.'
entity_type:
type: string
label: 'The entity type'
entity_field:
type: string
label: 'The corresponding entity field'
plugin_id:
type: string
label: 'The plugin ID'
views_argument:
type: views_handler
label: 'Argument'
mapping:
default_action:
type: string
label: 'When the filter value is NOT available'
exception:
type: mapping
label: 'Exception value'
mapping:
value:
type: string
label: 'Value'
title_enable:
type: boolean
label: 'Override title'
title:
type: label
label: 'Title'
title_enable:
type: boolean
label: 'Override title'
title:
type: label
label: 'Overridden title'
default_argument_type:
type: string
label: 'Type'
constraints:
PluginExists:
manager: plugin.manager.views.argument_default
default_argument_options:
type: views.argument_default.[%parent.default_argument_type]
label: 'Default argument options'
summary_options:
type: views.style.[%parent.summary.format]
label: 'Summary options'
summary:
type: mapping
label: 'Display a summary'
mapping:
sort_order:
type: string
label: 'Sort order'
number_of_records:
type: integer
label: 'Sort by'
format:
type: string
label: 'Format'
specify_validation:
type: boolean
label: 'Specify validation criteria'
validate:
type: mapping
label: 'Validation settings'
mapping:
type:
type: string
label: 'Validator'
constraints:
PluginExists:
manager: plugin.manager.views.argument_validator
fail:
type: string
label: 'Action to take if filter value does not validate'
validate_options:
type: views.argument_validator.[%parent.validate.type]
label: 'Validate options'
glossary:
type: boolean
label: 'Glossary mode'
limit:
type: integer
label: 'Character limit'
case:
type: string
label: 'Case'
path_case:
type: string
label: 'Case in path'
transform_dash:
type: boolean
label: 'Transform spaces to dashes in URL'
break_phrase:
type: boolean
label: 'Allow multiple values'
plugin_id:
type: string
label: 'Plugin ID'
constraints:
PluginExists:
manager: plugin.manager.views.argument
# @todo Remove this line and fix all views in core which use invalid
# argument plugins in https://drupal.org/i/3387325.
allowFallback: true
views_exposed_form:
type: mapping
mapping:
submit_button:
type: label
label: 'Submit button text'
reset_button:
type: boolean
label: 'Include reset button'
reset_button_label:
type: label
label: 'Reset button label'
exposed_sorts_label:
type: label
label: 'Exposed sorts label'
expose_sort_order:
type: boolean
label: 'Expose sort order'
sort_asc_label:
type: label
label: 'Ascending'
sort_desc_label:
type: label
label: 'Descending'
views_field:
type: views_handler
mapping:
label:
type: label
label: 'Create a label'
exclude:
type: boolean
label: 'Exclude from display'
alter:
type: mapping
label: 'Rewrite results'
mapping:
alter_text:
type: boolean
label: 'Override the output of this field with custom text'
text:
type: text
label: 'Text'
make_link:
type: boolean
label: 'Output this field as a custom link'
path:
type: text
label: 'Link path'
absolute:
type: boolean
label: 'Use absolute path'
external:
type: boolean
label: 'External server URL'
replace_spaces:
type: boolean
label: 'Replace spaces with dashes'
path_case:
type: string
label: 'Transform the case'
trim_whitespace:
type: boolean
label: 'Remove whitespace'
alt:
type: label
label: 'Title text'
rel:
type: string
label: 'Rel Text'
link_class:
type: string
label: 'Link class'
prefix:
type: label
label: 'Prefix text'
suffix:
type: label
label: 'Suffix text'
target:
type: string
label: 'Target'
nl2br:
type: boolean
label: 'Convert newlines to HTML <br> tags'
max_length:
type: integer
label: 'Maximum number of characters'
word_boundary:
type: boolean
label: 'Trim only on a word boundary'
ellipsis:
type: boolean
label: 'Add "…" at the end of trimmed text'
more_link:
type: boolean
label: 'Add a read-more link if output is trimmed'
more_link_text:
type: label
label: 'More link label'
more_link_path:
type: string
label: 'More link path'
strip_tags:
type: boolean
label: 'Strip HTML tags'
trim:
type: boolean
label: 'Trim this field to a maximum number of characters'
preserve_tags:
type: string
label: 'Preserve certain tags'
html:
type: boolean
label: 'Field can contain HTML'
element_type:
type: string
label: 'HTML element'
element_class:
type: string
label: 'CSS class'
element_label_type:
type: string
label: 'Label HTML element'
element_label_class:
type: string
label: 'CSS class'
element_label_colon:
type: boolean
label: 'Place a colon after the label'
element_wrapper_type:
type: string
label: 'Wrapper HTML element'
element_wrapper_class:
type: string
label: 'CSS class'
element_default_classes:
type: boolean
label: 'Add default classes'
empty:
type: string
label: 'No results text'
hide_empty:
type: boolean
label: 'Hide if empty'
empty_zero:
type: boolean
label: 'Count the number 0 as empty'
hide_alter_empty:
type: boolean
label: 'Hide rewriting if empty'
destination:
type: boolean
label: 'Append a destination query string to operation links.'
plugin_id:
type: string
label: 'Plugin ID'
constraints:
PluginExists:
manager: plugin.manager.views.field
# @todo Remove this line and fix all views in core which use invalid
# field plugins in https://drupal.org/i/3387325.
allowFallback: true
views_pager:
type: mapping
label: 'Pager'
mapping:
offset:
type: integer
label: 'Offset'
pagination_heading_level:
type: string
label: 'Pager header element'
items_per_page:
type: integer
label: 'Items per page'
views_pager_sql:
type: views_pager
label: 'SQL pager'
mapping:
items_per_page:
type: integer
label: 'Items per page'
pagination_heading_level:
type: string
label: 'Pager header element'
total_pages:
type: integer
label: 'Number of pages'
id:
type: integer
label: 'Pager ID'
tags:
type: mapping
label: 'Pager link labels'
mapping:
next:
type: label
label: 'Next page link text'
previous:
type: label
label: 'Previous page link text'
quantity:
type: integer
label: 'Number of pager links visible'
expose:
type: mapping
label: 'Exposed options'
mapping:
items_per_page:
type: boolean
label: 'Items per page'
items_per_page_label:
type: label
label: 'Items per page label'
items_per_page_options:
type: string
label: 'Exposed items per page options'
items_per_page_options_all:
type: boolean
label: 'Include all items option'
items_per_page_options_all_label:
type: label
label: 'All items label'
offset:
type: boolean
label: 'Expose Offset'
offset_label:
type: label
label: 'Offset label'
views_style:
type: mapping
mapping:
grouping:
type: sequence
label: 'Grouping field number %i'
sequence:
type: mapping
label: 'Field'
mapping:
field:
type: string
label: 'Field'
rendered:
type: boolean
label: 'Use rendered output to group rows'
rendered_strip:
type: boolean
label: 'Remove tags from rendered output'
row_class:
type: string
label: 'Row class'
default_row_class:
type: boolean
label: 'Add views row classes'
uses_fields:
type: boolean
label: 'Force using fields'
views_filter:
type: views_handler
mapping:
operator:
type: string
label: 'Operator'
value:
type: views.filter_value.[%parent.plugin_id]
label: 'Value'
group:
type: integer
label: 'Group'
exposed:
type: boolean
label: 'Expose this filter to visitors, to allow them to change it'
expose:
type: mapping
label: 'Expose'
mapping:
operator_id:
type: string
label: 'Operator identifier'
label:
type: label
label: 'Label'
description:
type: label
label: 'Description'
use_operator:
type: boolean
label: 'Expose operator'
operator:
type: string
label: 'Operator'
operator_limit_selection:
type: boolean
label: 'Limit the available operators'
operator_list:
type: sequence
label: 'List of available operators'
sequence:
type: string
label: 'Operator'
identifier:
type: string
label: 'Filter identifier'
required:
type: boolean
label: 'Required'
remember:
type: boolean
label: 'Remember the last selection'
multiple:
type: boolean
label: 'Allow multiple selections'
remember_roles:
type: sequence
label: 'User roles'
sequence:
type: string
label: 'Role'
is_grouped:
type: boolean
label: 'Grouped filters'
group_info:
type: mapping
label: 'Group'
mapping:
label:
type: label
label: 'Label'
description:
type: label
label: 'Description'
identifier:
type: string
label: 'Identifier'
optional:
type: boolean
label: 'Optional'
widget:
type: string
label: 'Widget type'
multiple:
type: boolean
label: 'Allow multiple selections'
remember:
type: boolean
label: 'Remember'
default_group:
type: string
label: 'Default'
default_group_multiple:
type: sequence
label: 'Defaults'
sequence:
type: integer
label: 'Default'
group_items:
type: sequence
label: 'Group items'
sequence:
type: views.filter.group_item.[%parent.%parent.%parent.plugin_id]
label: 'Group item'
plugin_id:
type: string
label: 'Plugin ID'
constraints:
PluginExists:
manager: plugin.manager.views.filter
# @todo Remove this line and fix all views in core which use invalid
# filter plugins in https://drupal.org/i/3387325.
allowFallback: true
views_filter_group_item:
type: mapping
label: 'Group item'
mapping:
title:
type: label
label: 'Label'
operator:
type: string
label: 'Operator'
value:
type: views.filter_value.[%parent.%parent.%parent.%parent.plugin_id]
label: 'Value'
views_relationship:
type: views_handler
mapping:
admin_label:
type: string
label: 'Administrative title'
required:
type: boolean
label: 'Require this relationship'
plugin_id:
type: string
label: 'The plugin ID'
constraints:
PluginExists:
manager: plugin.manager.views.relationship
# @todo Remove this line and fix all views in core which use invalid
# relationship plugins in https://drupal.org/i/3387325.
allowFallback: true
views_query:
type: mapping
label: 'Query options'
views_row:
type: mapping
label: 'Row options'
mapping:
relationship:
type: string
label: 'Relationship'
views_entity_row:
type: views_row
mapping:
view_mode:
type: string
label: 'View mode'
views_cache:
type: mapping
label: 'Cache configuration'
mapping:
type:
type: string
label: 'Cache type'
constraints:
PluginExists:
manager: plugin.manager.views.cache
views_display_extender:
type: mapping
label: 'Display extender settings'
views_field_bulk_form:
type: views_field
label: 'Bulk operation'
mapping:
action_title:
type: label
label: 'Action title'
include_exclude:
type: string
label: 'Available actions'
selected_actions:
type: sequence
label: 'Available actions'
sequence:
type: string
label: 'Action'

View File

@ -0,0 +1,143 @@
# Schema for the views display plugins.
views.display.default:
type: views_display
label: 'Default display options'
views_display_path:
type: views_display
mapping:
path:
type: string
label: 'Page path'
route_name:
type: string
label: 'Route name'
views.display.page:
type: views_display_path
label: 'Page display options'
mapping:
menu:
type: mapping
label: 'Menu'
mapping:
type:
type: string
label: 'Type'
title:
type: text
label: 'Title'
description:
type: text
label: 'Description'
weight:
type: weight
label: 'Weight'
enabled:
type: boolean
label: 'Enabled'
expanded:
type: boolean
label: 'Expanded'
menu_name:
type: string
label: 'Menu name'
parent:
type: string
label: 'Parent'
context:
type: string
label: 'Context'
tab_options:
type: mapping
label: 'Tab options'
mapping:
type:
type: string
label: 'Type'
title:
type: text
label: 'Title'
description:
type: text
label: 'Description'
weight:
type: weight
label: 'Weight'
menu_name:
type: string
label: 'Menu name'
use_admin_theme:
type: boolean
nullable: true
label: 'Use the administration theme when rendering the view page'
views.display.block:
type: views_display
label: 'Block display options'
mapping:
block_description:
type: label
label: 'Block name'
block_category:
type: text
label: 'Block category'
block_hide_empty:
type: boolean
label: 'Hide block if no result/empty text'
allow:
type: mapping
label: 'Allow'
mapping:
items_per_page:
type: boolean
label: 'Items per page'
views.display.feed:
type: views_display_path
label: 'Feed display options'
mapping:
sitename_title:
type: boolean
label: 'Use the site name for the title'
displays:
type: sequence
label: 'The feed icon will be available only to the selected displays.'
sequence:
type: string
label: 'Display'
views.display.embed:
type: views_display
label: 'Embed display options'
views.display.attachment:
type: views_display
label: 'Attachment display options'
mapping:
displays:
type: sequence
label: 'Attach to'
sequence:
type: string
label: 'Display'
attachment_position:
type: string
label: 'Attachment position'
inherit_arguments:
type: boolean
label: 'Inherit contextual filters'
inherit_exposed_filters:
type: boolean
label: 'Inherit exposed filters'
inherit_pager:
type: boolean
label: 'Inherit pager'
render_pager:
type: boolean
label: 'Render pager'
views.display.entity_reference:
type: views_display
label: 'Entity Reference'

View File

@ -0,0 +1,22 @@
# Schema for the entity reference 'views' selection handler settings.
entity_reference_selection.views:
type: entity_reference_selection
label: 'Views selection handler settings'
mapping:
view:
type: mapping
label: 'View used to select the entities'
mapping:
view_name:
type: string
label: 'View name'
display_name:
type: string
label: 'Display name'
arguments:
type: sequence
label: 'View arguments'
sequence:
type: string
label: 'Argument'

View File

@ -0,0 +1,16 @@
# Schema for the views exposed form.
views.exposed_form.basic:
type: views_exposed_form
label: 'Basic'
views.exposed_form.input_required:
type: views_exposed_form
label: 'Input required'
mapping:
text_input_required:
type: text
label: 'Text on demand'
text_input_required_format:
type: string
label: 'Text on demand format'

View File

@ -0,0 +1,262 @@
# Schema for the views field plugins.
views.field.*:
type: views_field
label: 'Default field'
views.field.boolean:
type: views_field
label: 'Boolean'
mapping:
type:
type: string
label: 'Output format'
type_custom_true:
type: label
label: 'Custom output for TRUE'
type_custom_false:
type: label
label: 'Custom output for FALSE'
not:
type: boolean
label: 'Reverse'
views.field.broken:
type: views_field
label: 'Broken'
views.field.counter:
type: views_field
label: 'Counter'
mapping:
counter_start:
type: integer
label: 'Starting value'
views.field.custom:
type: views_field
label: 'Custom'
views.field.date:
type: views_field
label: 'Date'
mapping:
date_format:
type: string
label: 'Date format'
custom_date_format:
type: string
label: 'Custom date format'
timezone:
type: string
label: 'Timezone'
views.field.entity_label:
type: views_field
label: 'Entity label'
mapping:
link_to_entity:
type: boolean
label: 'Link to entity'
views.field.file_size:
type: views_field
label: 'File size'
mapping:
file_size_display:
type: string
label: 'File size display'
views.field.links:
type: views_field
label: 'Links'
mapping:
fields:
type: sequence
label: 'Fields'
sequence:
type: string
label: 'Field'
destination:
type: boolean
label: 'Include destination'
views.field.dropbutton:
type: views.field.links
label: 'Drop button'
views.field.machine_name:
type: views_field
label: 'Machine name'
mapping:
machine_name:
type: boolean
label: 'Output machine name'
views.field.markup:
type: views_field
label: 'Markup'
views.field.numeric:
type: views_field
label: 'Numeric'
mapping:
set_precision:
type: boolean
label: 'Round'
precision:
type: integer
label: 'Precision'
decimal:
type: string
label: 'Decimal point'
separator:
type: string
label: 'Thousands marker'
format_plural:
type: boolean
label: 'Format plural'
format_plural_string:
type: plural_label
label: 'Plural variants'
prefix:
type: label
label: 'Prefix'
suffix:
type: label
label: 'Suffix'
views.field.prerender_list:
type: views_field
label: 'List'
mapping:
type:
type: string
label: 'Display type'
separator:
type: string
label: 'Separator'
views.field.serialized:
type: views_field
label: 'Serialized'
mapping:
format:
type: string
label: 'Display format'
key:
type: string
label: 'Which key should be displayed'
views.field.standard:
type: views_field
label: 'Standard'
views.field.time_interval:
type: views_field
label: 'Time interval'
mapping:
granularity:
type: integer
label: 'Granularity'
views.field.url:
type: views_field
label: 'URL'
mapping:
display_as_link:
type: boolean
label: 'Display as link'
views.field.language:
type: views_field
label: 'Language'
mapping:
native_language:
type: boolean
label: 'Display in native language'
views.field.rendered_entity:
type: views_field
label: 'Rendered entity'
mapping:
view_mode:
type: string
label: 'View mode'
views.field.entity_link:
type: views_field
label: 'Entity link'
mapping:
text:
type: label
label: 'Text to display'
output_url_as_text:
type: boolean
label: 'Output the URL as text'
absolute:
type: boolean
label: 'Output an absolute link'
views.field.entity_link_delete:
type: views.field.entity_link
label: 'Entity delete link'
views.field.entity_link_edit:
type: views.field.entity_link
label: 'Entity edit link'
views.field.bulk_form:
type: views_field_bulk_form
label: 'Bulk form'
views.field.field:
type: views_field
label: 'Views entity field handler'
mapping:
click_sort_column:
type: string
label: 'Column used for click sorting'
type:
type: string
label: 'Formatter'
settings:
label: 'Settings'
type: field.formatter.settings.[%parent.type]
group_column:
type: string
label: 'Group by column'
group_columns:
type: sequence
label: 'Group by columns'
sequence:
type: string
label: 'Column'
group_rows:
type: boolean
label: 'Display all values in the same row'
delta_limit:
type: integer
label: 'Field'
delta_offset:
type: integer
label: 'Offset'
delta_reversed:
type: boolean
label: 'Reversed'
delta_first_last:
type: boolean
label: 'First and last only'
multi_type:
type: string
label: 'Display type'
separator:
type: label
label: 'Separator'
field_api_classes:
type: boolean
label: 'Use field template'
views.field.field_language:
type: views.field.field
label: 'Views language field handler'

View File

@ -0,0 +1,210 @@
# Schema for the views filter plugins.
views.filter.*:
type: views_filter
label: 'Default filter'
views.filter.boolean:
type: views_filter
label: 'Boolean'
views_filter_boolean_string:
type: views_filter
label: 'Boolean string'
views.filter.broken:
type: views_filter
label: 'Broken'
views.filter.bundle:
type: views.filter.in_operator
label: 'Bundle'
views.filter.combine:
type: views.filter.string
label: 'Combine'
mapping:
fields:
type: sequence
label: 'Fields'
sequence:
type: string
label: 'Field'
views.filter_value.groupby_numeric:
type: views.filter_value.numeric
label: 'Group by numeric'
views.filter.in_operator:
type: views_filter
label: 'IN operator'
mapping:
operator:
type: string
label: 'Operator'
value:
type: sequence
label: 'Values'
sequence:
type: string
label: 'Value'
expose:
type: mapping
label: 'Expose'
mapping:
reduce:
type: boolean
label: 'Reduce'
group_info:
mapping:
group_items:
sequence:
type: views.filter.group_item.in_operator
label: 'Group item'
views.filter.string:
type: views_filter
label: 'String'
mapping:
expose:
type: mapping
label: 'Exposed'
mapping:
required:
type: boolean
label: 'Required'
placeholder:
type: label
label: 'Placeholder'
value:
type: string
label: 'Value'
views.filter.numeric:
type: views_filter
label: 'Numeric'
mapping:
expose:
type: mapping
label: 'Exposed'
mapping:
min_placeholder:
type: label
label: 'Min placeholder'
max_placeholder:
type: label
label: 'Max placeholder'
placeholder:
type: label
label: 'Placeholder'
views.filter_value.numeric:
type: mapping
label: 'Numeric'
mapping:
min:
type: string
label: 'Min'
max:
type: string
label: 'And max'
value:
type: string
label: 'Value'
views.filter_value.*:
type: string
label: 'Filter value'
views.filter_value.equality:
type: string
label: 'Equality'
views.filter.many_to_one:
type: views.filter.in_operator
label: 'Many to one'
mapping:
reduce_duplicates:
type: boolean
label: 'Reduce duplicate'
views.filter.entity_reference:
type: views.filter.many_to_one
label: 'Entity reference'
constraints:
FullyValidatable: ~
mapping:
sub_handler:
type: string
label: 'Selection handler'
constraints:
PluginExists:
manager: plugin.manager.entity_reference_selection
interface: 'Drupal\Core\Entity\EntityReferenceSelection\SelectionInterface'
widget:
type: string
label: 'Selection type'
sub_handler_settings:
type: entity_reference_selection.[%parent.sub_handler]
label: 'Selection handler settings'
views.filter.standard:
type: views_filter
label: 'Standard'
# Schema for the views group items.
views.filter.group_item.*:
type: views_filter_group_item
label: 'Group item'
views.filter.group_item.boolean:
type: views_filter_group_item
mapping:
value:
type: views.filter_value.string
views.filter.group_item.in_operator:
type: views_filter_group_item
mapping:
value:
type: views.filter_value.in_operator
# Schema for the views filter value.
views.filter_value.string:
type: string
views.filter_value.boolean:
type: string
views.filter_value.combine:
type: string
views.filter.language:
type: views.filter.in_operator
label: 'Language'
views.filter.latest_revision:
type: views_filter
label: 'Latest revision'
views.filter_value.date:
type: views.filter_value.numeric
label: 'Date'
mapping:
type:
type: string
label: 'Type'
views.filter.date:
type: views.filter.numeric
label: 'Date'
mapping:
type:
type: string
label: 'Type'
views.filter_value.in_operator:
type: sequence
label: 'Values'
sequence:
type: string
label: 'Value'

View File

@ -0,0 +1,35 @@
# Schema for the views pager plugins.
views.pager.*:
type: views_pager
label: 'Default pager'
views.pager.none:
type: views_pager
label: 'Display all items'
views.pager.some:
type: views_pager
label: 'Display a specified number of items'
views.pager.mini:
type: views_pager_sql
label: 'Paged output, mini pager'
views.pager.full:
type: views_pager_sql
label: 'Paged output, full pager'
mapping:
tags:
type: mapping
label: 'Tags'
mapping:
first:
type: label
label: 'First page link text'
last:
type: label
label: 'Last page link text'
quantity:
type: integer
label: 'Number of pager links visible'

View File

@ -0,0 +1,24 @@
# Schema for the views query.
views.query.views_query:
type: views_query
label: 'Views query'
mapping:
query_comment:
type: string
label: 'Query comment'
disable_sql_rewrite:
type: boolean
label: 'Disable SQL rewriting'
distinct:
type: boolean
label: 'Distinct'
replica:
type: boolean
label: 'Use Replica Server'
query_tags:
type: sequence
label: 'Query Tags'
sequence:
type: string
label: 'Tag'

View File

@ -0,0 +1,33 @@
# Schema for the views relationship.
views.relationship.*:
type: views_relationship
label: 'Standard'
views.relationship.standard:
type: views_relationship
label: 'Standard'
views.relationship.broken:
type: views_relationship
label: 'Broken'
views.relationship.groupwise_max:
type: views_relationship
label: 'Groupwise max'
mapping:
subquery_sort:
type: string
label: 'Representative sort criteria'
subquery_order:
type: string
label: 'Representative sort order'
subquery_regenerate:
type: boolean
label: 'Generate subquery each time view is run'
subquery_view:
type: string
label: 'Representative view'
subquery_namespace:
type: string
label: 'Subquery namespace'

View File

@ -0,0 +1,91 @@
# Schema for the views row.
views.row.*:
type: views_row
views.row.entity:*:
type: views_entity_row
label: 'Entity options'
views.row.fields:
type: views_row
label: 'Field options'
mapping:
default_field_elements:
type: boolean
label: 'Provide default field wrapper elements'
inline:
type: sequence
label: 'Inline'
sequence:
type: string
label: 'Inline'
separator:
type: string
label: 'Separator'
hide_empty:
type: boolean
label: 'Hide empty'
views.row.rss_fields:
type: views_row
label: 'RSS field options'
mapping:
title_field:
type: string
label: 'Title field'
link_field:
type: string
label: 'Link field'
description_field:
type: string
label: 'Description field'
creator_field:
type: string
label: 'Creator field'
date_field:
type: string
label: 'Publication date field'
guid_field_options:
type: mapping
label: 'Guid settings'
mapping:
guid_field:
type: string
label: 'GUID field'
guid_field_is_permalink:
type: boolean
label: 'GUID is permalink'
views.row.opml_fields:
type: views_row
label: 'OPML field options'
mapping:
type_field:
type: string
label: 'Type attribute'
text_field:
type: string
label: 'Text attribute'
created_field:
type: string
label: 'Created attribute'
description_field:
type: string
label: 'Description attribute'
html_url_field:
type: string
label: 'HTML URL attribute'
language_field:
type: string
label: 'Language attribute'
xml_url_field:
type: string
label: 'XML URL attribute'
url_field:
type: string
label: 'URL attribute'
views.row.entity_reference:
type: views.row.fields
label: 'Entity Reference inline fields'

View File

@ -0,0 +1,168 @@
# Schema for the configuration files of the Views module.
views.settings:
type: config_object
label: 'Views settings'
mapping:
display_extenders:
type: sequence
label: 'Display extenders'
sequence:
type: string
label: 'Display extender'
sql_signature:
type: boolean
label: 'Add Views signature to all SQL queries'
ui:
type: mapping
label: 'UI settings'
mapping:
show:
type: mapping
label: 'Live preview settings'
mapping:
additional_queries:
type: boolean
label: 'Show other queries run during render during live preview'
advanced_column:
type: boolean
label: 'Always show advanced display settings'
default_display:
type: boolean
label: 'Always show the default display'
performance_statistics:
type: boolean
label: 'Show performance statistics'
preview_information:
type: boolean
label: 'Show information and statistics about the view during live preview'
sql_query:
type: mapping
label: 'Query settings'
mapping:
enabled:
type: boolean
label: 'Show the SQL query'
where:
type: string
label: 'Show SQL query'
display_embed:
type: boolean
label: 'Allow embedded displays'
always_live_preview:
type: boolean
label: 'Automatically update preview on changes'
exposed_filter_any_label:
type: string
label: 'Label for "Any" value on non-required single-select exposed filters'
field_rewrite_elements:
type: sequence
label: 'Field rewrite elements'
sequence:
type: string
label: 'Element'
views.view.*:
type: config_entity
label: 'View'
mapping:
id:
type: machine_name
label: 'ID'
constraints:
Length:
# View IDs are specifically limited to 128 characters.
# @see \Drupal\views_ui\ViewAddForm::form()
max: 128
label:
type: required_label
label: 'Label'
module:
type: string
label: 'Module'
description:
type: text
label: 'Administrative description'
tag:
type: string
label: 'Tag'
base_table:
type: string
label: 'Base table'
base_field:
type: string
label: 'Base field'
display:
type: sequence
label: 'Displays'
sequence:
type: mapping
label: 'Display settings'
mapping:
id:
type: string
label: 'Machine name'
display_title:
type: text
label: 'Title'
display_plugin:
type: string
label: 'Display plugin'
constraints:
PluginExists:
manager: plugin.manager.views.display
position:
type: integer
label: 'Position'
display_options:
type: views.display.[%parent.display_plugin]
cache_metadata:
type: mapping
label: 'Cache metadata'
mapping:
max-age:
type: integer
label: 'Cache maximum age'
contexts:
type: sequence
label: 'Cache contexts'
sequence:
type: string
tags:
type: sequence
label: 'Cache tags'
sequence:
type: string
# Deprecated.
cacheable:
type: boolean
label: 'Cacheable'
views_block:
type: block_settings
label: 'View block'
constraints:
FullyValidatable: ~
mapping:
views_label:
type: label
label: 'Title'
requiredKey: false
items_per_page:
type: integer
label: 'Items per block'
constraints:
Range:
min: 1
# Will only be respected if the associated View is configured to allow this to be overridden.
# @see \Drupal\views\Plugin\views\display\Block::blockForm()
requiredKey: false
# NULL to use the default defined by the view.
nullable: true
block.settings.views_block:*:
type: views_block
constraints:
FullyValidatable: ~
block.settings.views_exposed_filter_block:*:
type: views_block

View File

@ -0,0 +1,51 @@
# Schema for the views sort plugins.
views.sort.*:
type: views_sort
label: 'Default sort'
views.sort.boolean:
type: views_sort
label: 'Boolean sort'
views.sort.date:
type: views_sort
label: 'Date sort'
mapping:
granularity:
type: string
label: 'Granularity'
views.sort.broken:
type: views_sort
label: 'Broken'
views.sort.random:
type: views_sort
label: 'Random'
views.sort.standard:
type: views_sort
label: 'Standard'
# Schema for the views sort expose.
views.sort_expose.*:
type: views_sort_expose
label: 'Fallback sort expose settings'
views.sort_expose.boolean:
type: views_sort_expose
label: 'Boolean sort expose settings'
views.sort_expose.date:
type: views_sort_expose
label: 'Date sort expose settings'
views.sort_expose.standard:
type: views_sort_expose
label: 'Standard sort expose settings'
views.sort_expose.random:
type: views.sort_expose.standard
label: 'Random sort expose settings'

View File

@ -0,0 +1,176 @@
# Schema for the views style plugins.
views.style.*:
type: views_style
label: 'Default style'
views.style.default:
type: views_style
label: 'Unformatted list'
views.style.html_list:
type: views_style
label: 'HTML List'
mapping:
type:
type: string
label: 'List type'
wrapper_class:
type: string
label: 'Wrapper class'
class:
type: string
label: 'List class'
views.style.grid:
type: views_style
label: 'Grid'
mapping:
columns:
type: integer
label: 'Number of columns'
automatic_width:
type: boolean
label: 'Automatic width'
alignment:
type: string
label: 'Alignment'
row_class_custom:
type: string
label: 'Custom row classes'
row_class_default:
type: boolean
label: 'Default views row classes'
col_class_custom:
type: string
label: 'Custom column classes'
col_class_default:
type: boolean
label: 'Default views column classes'
views.style.grid_responsive:
type: views_style
label: 'Grid - Responsive'
mapping:
columns:
type: integer
label: 'Maximum number of columns'
cell_min_width:
type: integer
label: 'Minimum cell width'
grid_gutter:
type: integer
label: 'Grid gutter'
alignment:
type: string
label: 'Alignment'
views.style.table:
type: views_style
label: 'Table'
mapping:
columns:
type: sequence
label: 'Columns'
sequence:
type: string
label: 'Columns name'
default:
type: string
label: 'Default sort'
info:
type: sequence
label: 'Columns info'
sequence:
type: mapping
label: 'Column info'
mapping:
sortable:
type: boolean
label: 'Sortable'
default_sort_order:
type: string
label: 'Default order'
align:
type: string
label: 'Align'
separator:
type: string
label: 'Separator'
empty_column:
type: boolean
label: 'Hide empty columns'
responsive:
type: string
label: 'Responsive'
override:
type: boolean
label: 'Override normal sorting if click sorting is used'
sticky:
type: boolean
label: 'Enable Drupal style "sticky" table headers'
summary:
type: label
label: 'Summary title'
order:
type: string
label: 'Default order'
empty_table:
type: boolean
label: 'Show the empty text in the table'
caption:
type: label
label: 'Caption for the table'
description:
type: text
label: 'Table description'
class:
type: string
label: 'Table class'
views.style.default_summary:
type: views_style
label: 'Summary options'
mapping:
base_path:
type: string
label: 'Base path'
count:
type: boolean
label: 'Display record count with link'
override:
type: boolean
label: 'Override number of items to display'
items_per_page:
type: integer
label: 'Items to display'
views.style.rss:
type: views_style
label: 'RSS Feed'
mapping:
description:
type: label
label: 'RSS description'
views.style.unformatted_summary:
type: views.style.default_summary
label: 'Unformatted'
mapping:
inline:
type: boolean
label: 'Display items inline'
separator:
type: string
label: 'Separator'
views.style.entity_reference:
type: views_style
label: 'Entity Reference list'
mapping:
search_fields:
type: sequence
label: 'Search fields'
sequence:
type: string
label: 'Search field'

View File

@ -0,0 +1,36 @@
/**
* CSS for Views responsive grid style.
*/
.views-view-responsive-grid {
--views-responsive-grid--layout-gap: 10px; /* Will be overridden by an inline style. */
--views-responsive-grid--column-count: 4; /* Will be overridden by an inline style. */
--views-responsive-grid--cell-min-width: 100px; /* Will be overridden by an inline style. */
}
.views-view-responsive-grid--horizontal {
/**
* Calculated values.
*/
--views-responsive-grid--gap-count: calc(var(--views-responsive-grid--column-count) - 1);
--views-responsive-grid--total-gap-width: calc(var(--views-responsive-grid--gap-count) * var(--views-responsive-grid--layout-gap));
--views-responsive-grid-item--max-width: calc((100% - var(--views-responsive-grid--total-gap-width)) / var(--views-responsive-grid--column-count));
--views-responsive-grid-item--calculated-min-width: min(100%, var(--views-responsive-grid--cell-min-width)); /* Ensure that cell minimum width does not overflow container. */
display: grid;
grid-template-columns: repeat(auto-fill, minmax(max(var(--views-responsive-grid-item--calculated-min-width), var(--views-responsive-grid-item--max-width)), 1fr));
gap: var(--views-responsive-grid--layout-gap);
}
.views-view-responsive-grid--vertical {
margin-bottom: calc(var(--views-responsive-grid--layout-gap) * -1); /* Offset the bottom row's padding. */
column-width: var(--views-responsive-grid--cell-min-width);
column-count: var(--views-responsive-grid--column-count);
column-gap: var(--views-responsive-grid--layout-gap);
}
.views-view-responsive-grid--vertical .views-view-responsive-grid__item > * {
padding-bottom: var(--views-responsive-grid--layout-gap);
page-break-inside: avoid;
break-inside: avoid;
}

View File

@ -0,0 +1,23 @@
/* table style column align */
.views-align-left {
text-align: left;
}
.views-align-right {
text-align: right;
}
.views-align-center {
text-align: center;
}
/* Grid style column align. */
.views-view-grid .views-col {
float: left;
}
.views-view-grid .views-row {
float: left;
clear: both;
width: 100%;
}
/* Provide some space between display links. */
.views-display-link + .views-display-link {
margin-left: 0.5em;
}

View File

@ -0,0 +1,34 @@
---
label: 'Managing content listings (views)'
top_level: true
related:
- block.overview
- views_ui.bulk_operations
- user.overview
---
<h2>{% trans %}What is a view?{% endtrans %}</h2>
<p>{% trans %}A <em>view</em> is a listing of items on your site; for example, a block showing the most recent comments, a page listing news items, or a list of registered users. The listings can be formatted in a table, grid, list, calendar, RSS feed, and other formats (some output formats may require you to install additional contributed modules).{% endtrans %}</p>
<h2>{% trans %}What are the components of a view?{% endtrans %}</h2>
<p>{% trans %}When you first create a view, you will specify what type of <em>base data</em> is being displayed in the view, which cannot be changed. After choosing a base data type, you can edit the following components, which allow you to specify which data to output, in what order, and in what format:{% endtrans %}</p>
<ul>
<li>{% trans %}<em>Displays</em>: whether the output goes to a page, block, feed, etc.; a single view can have multiple displays, each with different settings.{% endtrans %}</li>
<li>{% trans %}<em>Format</em>: the output style for each display, such as content item, grid, table, or HTML list.{% endtrans %}</li>
<li>{% trans %}<em>Fields</em>: if the Format allows, the particular fields to display.{% endtrans %}</li>
<li>{% trans %}<em>Filter criteria</em>: criteria to limit the data to output, such as whether the content is published, the type of content, etc. Filters can be <em>exposed</em> to let users choose how to filter the data.{% endtrans %}</li>
<li>{% trans %}<em>Sort criteria</em>: how to order the data. Sorting can also be exposed to users.{% endtrans %}</li>
<li>{% trans %}<em>Page settings</em>, <em>Block settings</em>, etc.: settings specific to the display type, such as the URL for a page display. Most display types support an <em>Access</em> setting, where you can choose a Permission or Role that a user must have in order to see the view.{% endtrans %}</li>
<li>{% trans %}<em>Header</em> and <em>Footer</em>: content to display at the top or bottom of the view display.{% endtrans %}</li>
<li>{% trans %}<em>No results behavior</em>: what to do if the filter criteria result in having no data to display.{% endtrans %}</li>
<li>{% trans %}<em>Pager</em>: how many items to display, and how to paginate if there are additional items to display.{% endtrans %}</li>
<li>{% trans %}<em>Advanced</em> &gt; <em>Contextual filters</em>: like regular filters, except the criteria come from the <em>context</em>, such as the current date, page the view is displayed on, etc.{% endtrans %}</li>
<li>{% trans %}<em>Advanced</em> &gt; <em>Relationships</em>: additional data to pull in and display, related in some way to the base data of the view (such as data about the user who created the content item).{% endtrans %}</li>
<li>{% trans %}<em>Advanced</em> &gt; <em>Exposed form</em>: if you have exposed filters or sorts, how to display the form to the user.{% endtrans %}</li>
</ul>
<h2>{% trans %}What are bulk operations?{% endtrans %}</h2>
<p>{% trans %}Views using a table display format can include a bulk operations form, which allows users with sufficient permission to select one or more items from the view and apply an administrative action to them. The bulk actions available are specific to the base data type of the view; for example, a view of content items could support bulk publishing and unpublishing actions. If you have the core Actions UI module installed, see the related topic "Configuring actions" for more about actions.{% endtrans %}</p>
<h2>{% trans %}Managing views overview{% endtrans %}</h2>
<p>{% trans %}The core Views module handles the display of views, and the core Views UI module allows you to create, edit, and delete views in the administrative interface. See the related topics listed below for specific tasks (if the Views UI module is installed).{% endtrans %}</p>
<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>

View File

@ -0,0 +1,210 @@
/**
* @file
* Handles AJAX fetching of views, including filter submission and response.
*/
(function ($, Drupal, drupalSettings) {
/**
* Attaches the AJAX behavior to exposed filters forms and key View links.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches ajaxView functionality to relevant elements.
*/
Drupal.behaviors.ViewsAjaxView = {};
Drupal.behaviors.ViewsAjaxView.attach = function (context, settings) {
if (settings?.views?.ajaxViews) {
const {
views: { ajaxViews },
} = settings;
Object.keys(ajaxViews || {}).forEach((i) => {
Drupal.views.instances[i] = new Drupal.views.ajaxView(ajaxViews[i]);
});
}
};
Drupal.behaviors.ViewsAjaxView.detach = (context, settings, trigger) => {
if (trigger === 'unload') {
if (settings?.views?.ajaxViews) {
const {
views: { ajaxViews },
} = settings;
Object.keys(ajaxViews || {}).forEach((i) => {
const selector = `.js-view-dom-id-${ajaxViews[i].view_dom_id}`;
if ($(selector, context).length) {
delete Drupal.views.instances[i];
delete settings.views.ajaxViews[i];
}
});
}
}
};
/**
* @namespace
*/
Drupal.views = {};
/**
* @type {object.<string, Drupal.views.ajaxView>}
*/
Drupal.views.instances = {};
/**
* JavaScript object for a certain view.
*
* @constructor
*
* @param {object} settings
* Settings object for the ajax view.
* @param {string} settings.view_dom_id
* The DOM id of the view.
*/
Drupal.views.ajaxView = function (settings) {
const selector = `.js-view-dom-id-${settings.view_dom_id}`;
this.$view = $(selector);
// Retrieve the path to use for views' ajax.
let ajaxPath = drupalSettings.views.ajax_path;
// If there are multiple views this might've ended up showing up multiple
// times.
if (ajaxPath.constructor.toString().includes('Array')) {
ajaxPath = ajaxPath[0];
}
// Check if there are any GET parameters to send to views.
let queryString = window.location.search || '';
if (queryString !== '') {
// Remove the question mark and Drupal path component if any.
queryString = queryString
.slice(1)
.replace(/q=[^&]+&?|page=[^&]+&?|&?render=[^&]+/, '');
if (queryString !== '') {
// If there is a '?' in ajaxPath, clean URL are on and & should be
// used to add parameters.
queryString = (/\?/.test(ajaxPath) ? '&' : '?') + queryString;
}
}
this.element_settings = {
url: ajaxPath + queryString,
submit: settings,
httpMethod: 'GET',
setClick: true,
event: 'click',
selector,
progress: { type: 'fullscreen' },
};
this.settings = settings;
// Add the ajax to exposed forms.
this.$exposed_form = $(
`form#views-exposed-form-${settings.view_name.replace(
/_/g,
'-',
)}-${settings.view_display_id.replace(/_/g, '-')}`,
);
once('exposed-form', this.$exposed_form).forEach(
this.attachExposedFormAjax.bind(this),
);
// Add the ajax to pagers.
once(
'ajax-pager',
this.$view
// Don't attach to nested views. Doing so would attach multiple behaviors
// to a given element.
.filter(this.filterNestedViews.bind(this)),
).forEach(this.attachPagerAjax.bind(this));
// Add a trigger to update this view specifically. In order to trigger a
// refresh use the following code.
//
// @code
// $('.view-name').trigger('RefreshView');
// @endcode
const selfSettings = $.extend({}, this.element_settings, {
event: 'RefreshView',
base: this.selector,
httpMethod: 'GET',
element: this.$view.get(0),
});
this.refreshViewAjax = Drupal.ajax(selfSettings);
};
/**
* @method
*/
Drupal.views.ajaxView.prototype.attachExposedFormAjax = function () {
const that = this;
this.exposedFormAjax = [];
// Exclude the reset buttons so no AJAX behaviors are bound. Many things
// break during the form reset phase if using AJAX.
$(
'input[type=submit], button[type=submit], input[type=image]',
this.$exposed_form,
)
.not('[data-drupal-selector=edit-reset]')
.each(function (index) {
const selfSettings = $.extend({}, that.element_settings, {
base: $(this).attr('id'),
element: this,
});
that.exposedFormAjax[index] = Drupal.ajax(selfSettings);
});
};
/**
* @return {boolean}
* If there is at least one parent with a view class return false.
*/
Drupal.views.ajaxView.prototype.filterNestedViews = function () {
// If there is at least one parent with a view class, this view
// is nested (e.g., an attachment). Bail.
return !this.$view.parents('.view').length;
};
/**
* Attach the ajax behavior to each link.
*/
Drupal.views.ajaxView.prototype.attachPagerAjax = function () {
this.$view
.find(
'.js-pager__items a, th.views-field a, .attachment .views-summary a',
)
.each(this.attachPagerLinkAjax.bind(this));
};
/**
* Attach the ajax behavior to a singe link.
*
* @param {string} [id]
* The ID of the link.
* @param {HTMLElement} link
* The link element.
*/
Drupal.views.ajaxView.prototype.attachPagerLinkAjax = function (id, link) {
const $link = $(link);
const viewData = {};
const href = $link.attr('href');
// Construct an object using the settings defaults and then overriding
// with data specific to the link.
$.extend(
viewData,
this.settings,
Drupal.Views.parseQueryString(href),
// Extract argument data from the URL.
Drupal.Views.parseViewArgs(href, this.settings.view_base_path),
);
const selfSettings = $.extend({}, this.element_settings, {
submit: viewData,
base: false,
element: link,
httpMethod: 'GET',
});
this.pagerAjax = Drupal.ajax(selfSettings);
};
})(jQuery, Drupal, drupalSettings);

View File

@ -0,0 +1,114 @@
/**
* @file
* Some basic behaviors and utility functions for Views.
*/
(function ($, Drupal, drupalSettings) {
/**
* @namespace
*/
Drupal.Views = {};
/**
* Helper function to parse a querystring.
*
* @param {string} query
* The querystring to parse.
*
* @return {object}
* A map of query parameters.
*/
Drupal.Views.parseQueryString = function (query) {
const args = {};
if (query.includes('?')) {
query = query.substring(query.indexOf('?') + 1);
}
let pair;
const pairs = query.split('&');
for (let i = 0; i < pairs.length; i++) {
pair = pairs[i].split('=');
// Ignore the 'q' path argument, if present.
if (pair[0] !== 'q') {
if (pair[1]) {
args[decodeURIComponent(pair[0].replace(/\+/g, ' '))] =
decodeURIComponent(pair[1].replace(/\+/g, ' '));
} else {
args[decodeURIComponent(pair[0].replace(/\+/g, ' '))] = '';
}
}
}
return args;
};
/**
* Helper function to return a view's arguments based on a path.
*
* @param {string} href
* The href to check.
* @param {string} viewPath
* The views path to check.
*
* @return {object}
* An object containing `view_args` and `view_path`.
*/
Drupal.Views.parseViewArgs = function (href, viewPath) {
const returnObj = {};
const path = Drupal.Views.getPath(href);
// Get viewPath URL without baseUrl portion.
const viewHref = Drupal.url(viewPath).substring(
drupalSettings.path.baseUrl.length,
);
// Ensure we have a correct path.
if (viewHref && path.startsWith(`${viewHref}/`)) {
returnObj.view_args = decodeURIComponent(
path.substring(viewHref.length + 1, path.length),
);
returnObj.view_path = path;
}
return returnObj;
};
/**
* Strip off the protocol plus domain from an href.
*
* @param {string} href
* The href to strip.
*
* @return {string}
* The href without the protocol and domain.
*/
Drupal.Views.pathPortion = function (href) {
// Remove e.g. http://example.com if present.
const protocol = window.location.protocol;
if (href.startsWith(protocol)) {
// 2 is the length of the '//' that normally follows the protocol.
href = href.substring(href.indexOf('/', protocol.length + 2));
}
return href;
};
/**
* Return the Drupal path portion of an href.
*
* @param {string} href
* The href to check.
*
* @return {string}
* An internal path.
*/
Drupal.Views.getPath = function (href) {
href = Drupal.Views.pathPortion(href);
href = href.substring(drupalSettings.path.baseUrl.length, href.length);
if (href.startsWith('?q=')) {
// 3 is the length of the '?q=' added to the URL without clean URLs.
href = href.substring(3, href.length);
}
const chars = ['#', '?', '&'];
for (let i = 0; i < chars.length; i++) {
if (href.includes(chars[i])) {
href = href.substring(0, href.indexOf(chars[i]));
}
}
return href;
};
})(jQuery, Drupal, drupalSettings);

View File

@ -0,0 +1,41 @@
<?php
namespace Drupal\views\Ajax;
use Drupal\Core\Ajax\CommandInterface;
/**
* Provides an AJAX command for highlighting a certain new piece of html.
*
* This command is implemented in Drupal.AjaxCommands.prototype.viewsHighlight.
*/
class HighlightCommand implements CommandInterface {
/**
* A CSS selector string.
*
* @var string
*/
protected $selector;
/**
* Constructs a \Drupal\views\Ajax\HighlightCommand object.
*
* @param string $selector
* A CSS selector.
*/
public function __construct($selector) {
$this->selector = $selector;
}
/**
* {@inheritdoc}
*/
public function render() {
return [
'command' => 'viewsHighlight',
'selector' => $this->selector,
];
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace Drupal\views\Ajax;
use Drupal\Core\Ajax\CommandInterface;
/**
* Provides an AJAX command for replacing the page title.
*
* This command is implemented in
* Drupal.AjaxCommands.prototype.viewsReplaceTitle.
*/
class ReplaceTitleCommand implements CommandInterface {
/**
* The page title to replace.
*
* @var string
*/
protected $title;
/**
* Constructs a \Drupal\views\Ajax\ReplaceTitleCommand object.
*
* @param string $title
* The title of the page.
*/
public function __construct($title) {
$this->title = $title;
}
/**
* {@inheritdoc}
*/
public function render() {
return [
'command' => 'viewsReplaceTitle',
'title' => $this->title,
'siteName' => \Drupal::config('system.site')->get('name'),
];
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace Drupal\views\Ajax;
use Drupal\Core\Ajax\CommandInterface;
/**
* Provides an AJAX command for showing the save and cancel buttons.
*
* This command is implemented in
* Drupal.AjaxCommands.prototype.viewsShowButtons.
*/
class ShowButtonsCommand implements CommandInterface {
/**
* Whether the view has been changed.
*
* @var bool
*/
protected $changed;
/**
* Constructs a \Drupal\views\Ajax\ShowButtonsCommand object.
*
* @param bool $changed
* Whether the view has been changed.
*/
public function __construct($changed) {
$this->changed = $changed;
}
/**
* {@inheritdoc}
*/
public function render() {
return [
'command' => 'viewsShowButtons',
'changed' => $this->changed,
];
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace Drupal\views\Ajax;
use Drupal\Core\Ajax\CommandInterface;
/**
* Provides an AJAX command for triggering the views live preview.
*
* This command is implemented in
* Drupal.AjaxCommands.prototype.viewsTriggerPreview.
*/
class TriggerPreviewCommand implements CommandInterface {
/**
* {@inheritdoc}
*/
public function render() {
return [
'command' => 'viewsTriggerPreview',
];
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace Drupal\views\Ajax;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\views\ViewExecutable;
/**
* Custom JSON response object for an ajax view response.
*
* We use a special response object to be able to fire a proper alter hook.
*/
class ViewAjaxResponse extends AjaxResponse {
/**
* The view executed on this ajax request.
*
* @var \Drupal\views\ViewExecutable
*/
protected $view;
/**
* Sets the executed view of this response.
*
* @param \Drupal\views\ViewExecutable $view
* The View executed on this ajax request.
*/
public function setView(ViewExecutable $view) {
$this->view = $view;
}
/**
* Gets the executed view of this response.
*
* @return \Drupal\views\ViewExecutable
* The View executed on this ajax request.
*/
public function getView() {
return $this->view;
}
}

View File

@ -0,0 +1,128 @@
<?php
namespace Drupal\views;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
* View analyzer plugin manager.
*
* This tool is a small plugin manager to perform analysis on a view and
* report results to the user. This tool is meant to let modules that
* provide data to Views also help users properly use that data by
* detecting invalid configurations. Views itself comes with only a
* small amount of analysis tools, but more could easily be added either
* by modules or as patches to Views itself.
*/
class Analyzer {
use StringTranslationTrait;
/**
* A module handler that invokes the 'views_analyze' hook.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* Constructs an Analyzer object.
*
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler that invokes the 'views_analyze' hook.
*/
public function __construct(ModuleHandlerInterface $module_handler) {
$this->moduleHandler = $module_handler;
}
/**
* Analyzes a review and return the results.
*
* @param \Drupal\views\ViewExecutable $view
* The view to analyze.
*
* @return array
* An array of analyze results organized into arrays keyed by 'ok',
* 'warning' and 'error'.
*/
public function getMessages(ViewExecutable $view) {
$view->initDisplay();
$messages = $this->moduleHandler->invokeAll('views_analyze', [$view]);
return $messages;
}
/**
* Formats the analyze result into a message string.
*
* This is based upon the format of
* \Drupal\Core\Messenger\MessengerInterface::addMessage() which uses separate
* boxes for "ok", "warning" and "error".
*/
public function formatMessages(array $messages) {
if (empty($messages)) {
$messages = [static::formatMessage($this->t('View analysis can find nothing to report.'), 'ok')];
}
$types = ['ok' => [], 'warning' => [], 'error' => []];
foreach ($messages as $message) {
if (empty($types[$message['type']])) {
$types[$message['type']] = [];
}
$types[$message['type']][] = $message['message'];
}
$output = '';
foreach ($types as $type => $messages) {
$type .= ' messages';
$message = '';
if (count($messages) > 1) {
$item_list = [
'#theme' => 'item_list',
'#items' => $messages,
];
$message = \Drupal::service('renderer')->render($item_list);
}
elseif ($messages) {
$message = array_shift($messages);
}
if ($message) {
$output .= "<div class=\"$type\">$message</div>";
}
}
return $output;
}
/**
* Formats an analysis message.
*
* This tool should be called by any module responding to the analyze hook
* to properly format the message. It is usually used in the form:
* @code
* $ret[] = Analyzer::formatMessage(t('This is the message'), 'ok');
* @endcode
*
* The 'ok' status should be used to provide information about things
* that are acceptable. In general analysis isn't interested in 'ok'
* messages, but instead the 'warning', which is a category for items
* that may be broken unless the user knows what they are doing, and 'error'
* for items that are definitely broken are much more useful.
*
* @param string $message
* The message.
* @param string $type
* The type of message. This should be "ok", "warning" or "error". Other
* values can be used but how they are treated by the output routine
* is undefined.
*
* @return array
* A single formatted message, consisting of a key message and a key type.
*/
public static function formatMessage($message, $type = 'error') {
return ['message' => $message, 'type' => $type];
}
}

View File

@ -0,0 +1,78 @@
<?php
namespace Drupal\views\Annotation;
/**
* Defines a Plugin annotation object for views access plugins.
*
* @see \Drupal\views\Plugin\views\access\AccessPluginBase
*
* @ingroup views_access_plugins
*
* @Annotation
*/
class ViewsAccess extends ViewsPluginAnnotationBase {
/**
* The plugin ID.
*
* @var string
*/
public $id;
/**
* The plugin title used in the views UI.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $title = '';
/**
* An optional short title used in the views UI.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $short_title = '';
/**
* A short help string; this is displayed in the views UI.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $help = '';
/**
* The types of the display this plugin can be used with.
*
* For example the Feed display defines the type 'feed', so only rss style
* and row plugins can be used in the views UI.
*
* @var array
*/
public $display_types;
/**
* The base tables on which this access plugin can be used.
*
* If no base table is specified the plugin can be used with all tables.
*
* @var array
*/
public $base;
/**
* Whether the plugin should be not selectable in the UI.
*
* If set to TRUE, you can still use it via the API in config files.
*
* @var bool
*/
public $no_ui;
}

View File

@ -0,0 +1,16 @@
<?php
namespace Drupal\views\Annotation;
/**
* Defines a Plugin annotation object for views area handlers.
*
* @see \Drupal\views\Plugin\views\area\AreaPluginBase
*
* @ingroup views_area_handlers
*
* @Annotation
*/
class ViewsArea extends ViewsHandlerAnnotationBase {
}

View File

@ -0,0 +1,16 @@
<?php
namespace Drupal\views\Annotation;
/**
* Defines a Plugin annotation object for views argument handlers.
*
* @see \Drupal\views\Plugin\views\argument\ArgumentPluginBase
*
* @ingroup views_argument_handlers
*
* @Annotation
*/
class ViewsArgument extends ViewsHandlerAnnotationBase {
}

View File

@ -0,0 +1,50 @@
<?php
namespace Drupal\views\Annotation;
/**
* Defines a Plugin annotation object for views argument default plugins.
*
* @see \Drupal\views\Plugin\views\argument_default\ArgumentDefaultPluginBase
*
* @ingroup views_argument_default_plugins
*
* @Annotation
*/
class ViewsArgumentDefault extends ViewsPluginAnnotationBase {
/**
* The plugin ID.
*
* @var string
*/
public $id;
/**
* The plugin title used in the views UI.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $title = '';
/**
* An optional short title used in the views UI.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $short_title = '';
/**
* Whether the plugin should be not selectable in the UI.
*
* If it's set to TRUE, you can still use it via the API in config files.
*
* @var bool
*/
public $no_ui;
}

View File

@ -0,0 +1,50 @@
<?php
namespace Drupal\views\Annotation;
/**
* Defines a Plugin annotation object for views argument validator plugins.
*
* @see \Drupal\views\Plugin\views\argument_validator\ArgumentValidatorPluginBase
*
* @ingroup views_argument_validate_plugins
*
* @Annotation
*/
class ViewsArgumentValidator extends ViewsPluginAnnotationBase {
/**
* The plugin ID.
*
* @var string
*/
public $id;
/**
* The plugin title used in the views UI.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $title = '';
/**
* An optional short title used in the views UI.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $short_title = '';
/**
* Whether the plugin should be not selectable in the UI.
*
* If it's set to TRUE, you can still use it via the API in config files.
*
* @var bool
*/
public $no_ui;
}

View File

@ -0,0 +1,78 @@
<?php
namespace Drupal\views\Annotation;
/**
* Defines a Plugin annotation object for views cache plugins.
*
* @see \Drupal\views\Plugin\views\cache\CachePluginBase
*
* @ingroup views_cache_plugins
*
* @Annotation
*/
class ViewsCache extends ViewsPluginAnnotationBase {
/**
* The plugin ID.
*
* @var string
*/
public $id;
/**
* The plugin title used in the views UI.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $title = '';
/**
* An optional short title used in the views UI.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $short_title = '';
/**
* A short help string; this is displayed in the views UI.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $help = '';
/**
* The types of the display this plugin can be used with.
*
* For example the Feed display defines the type 'feed', so only rss style
* and row plugins can be used in the views UI.
*
* @var array
*/
public $display_types;
/**
* The base tables on which this cache plugin can be used.
*
* If no base table is specified the plugin can be used with all tables.
*
* @var array
*/
public $base;
/**
* Whether the plugin should be not selectable in the UI.
*
* If it's set to TRUE, you can still use it via the API in config files.
*
* @var bool
*/
public $no_ui;
}

View File

@ -0,0 +1,129 @@
<?php
namespace Drupal\views\Annotation;
/**
* Defines a Plugin annotation object for views display plugins.
*
* @see \Drupal\views\Plugin\views\display\DisplayPluginBase
*
* @ingroup views_display_plugins
*
* @Annotation
*/
class ViewsDisplay extends ViewsPluginAnnotationBase {
/**
* The plugin ID.
*
* @var string
*/
public $id;
/**
* The plugin title used in the views UI.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $title = '';
/**
* An optional short title used in the views UI.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $short_title = '';
/**
* The administrative name of the display.
*
* The name is displayed on the Views overview and also used as default name
* for new displays.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $admin = '';
/**
* A short help string; this is displayed in the views UI.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $help = '';
/**
* Whether or not to use hook_menu() to register a route.
*
* @var bool
*/
public $uses_menu_links;
/**
* Does the display plugin registers routes to the route.
*
* @var bool
*/
public $uses_route;
/**
* Does the display plugin provide blocks.
*
* @var bool
*/
public $uses_hook_block;
/**
* A list of places where contextual links should be added.
*
* For example, ['page','block' ]
*
* If you don't specify it there will be contextual links rendered for all
* displays of a view. If this is not set or regions have been specified,
* views will display an option to 'hide contextual links'. Use an empty
* array to disable.
*
* @var string[]
*/
public $contextual_links_locations;
/**
* The base tables on which this display plugin can be used.
*
* If no base table is specified the plugin can be used with all tables.
*
* @var array
*/
public $base;
/**
* The theme function used to render the display's output.
*
* @var string
*/
public $theme;
/**
* Whether the plugin should be not selectable in the UI.
*
* If it's set to TRUE, you can still use it via the API in config files.
*
* @var bool
*/
public $no_ui;
/**
* Whether the display returns a response object.
*
* @var bool
*/
public $returns_response;
}

View File

@ -0,0 +1,59 @@
<?php
namespace Drupal\views\Annotation;
/**
* Defines a Plugin annotation object for views display extender plugins.
*
* @see \Drupal\views\Plugin\views\display_extender\DisplayExtenderPluginBase
*
* @ingroup views_display_extender_plugins
*
* @Annotation
*/
class ViewsDisplayExtender extends ViewsPluginAnnotationBase {
/**
* The plugin ID.
*
* @var string
*/
public $id;
/**
* The plugin title used in the views UI.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $title = '';
/**
* An optional short title used in the views UI.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $short_title = '';
/**
* A short help string; this is displayed in the views UI.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $help = '';
/**
* Whether or not the plugin is selectable in the UI.
*
* If it's set to TRUE, you can still use it via the API in config files.
*
* @var bool
*/
public $no_ui;
}

View File

@ -0,0 +1,79 @@
<?php
namespace Drupal\views\Annotation;
/**
* Defines a Plugin annotation object for views exposed form plugins.
*
* @see \Drupal\views\Plugin\views\exposed_form\ExposedFormPluginInterface
* @see \Drupal\views\Plugin\views\exposed_form\ExposedFormPluginBase
*
* @ingroup views_exposed_form_plugins
*
* @Annotation
*/
class ViewsExposedForm extends ViewsPluginAnnotationBase {
/**
* The plugin ID.
*
* @var string
*/
public $id;
/**
* The plugin title used in the views UI.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $title = '';
/**
* An optional short title used in the views UI.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $short_title = '';
/**
* A short help string; this is displayed in the views UI.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $help = '';
/**
* The types of the display this plugin can be used with.
*
* For example the Feed display defines the type 'feed', so only rss style
* and row plugins can be used in the views UI.
*
* @var array
*/
public $display_types;
/**
* The base tables on which this exposed form plugin can be used.
*
* If no base table is specified the plugin can be used with all tables.
*
* @var array
*/
public $base;
/**
* Whether the plugin should be not selectable in the UI.
*
* If it's set to TRUE, you can still use it via the API in config files.
*
* @var bool
*/
public $no_ui;
}

View File

@ -0,0 +1,16 @@
<?php
namespace Drupal\views\Annotation;
/**
* Defines a Plugin annotation object for views field handlers.
*
* @see \Drupal\views\Plugin\views\field\FieldPluginBase
*
* @ingroup views_field_handlers
*
* @Annotation
*/
class ViewsField extends ViewsHandlerAnnotationBase {
}

View File

@ -0,0 +1,16 @@
<?php
namespace Drupal\views\Annotation;
/**
* Defines a Plugin annotation object for views filter handlers.
*
* @see \Drupal\views\Plugin\views\filter\FilterPluginBase
*
* @ingroup views_filter_handlers
*
* @Annotation
*/
class ViewsFilter extends ViewsHandlerAnnotationBase {
}

View File

@ -0,0 +1,12 @@
<?php
namespace Drupal\views\Annotation;
use Drupal\Component\Annotation\PluginID;
/**
* Defines an abstract base class for all views handler annotations.
*/
abstract class ViewsHandlerAnnotationBase extends PluginID {
}

View File

@ -0,0 +1,16 @@
<?php
namespace Drupal\views\Annotation;
/**
* Defines a Plugin annotation object for views join plugins.
*
* @see \Drupal\views\Plugin\views\join\JoinPluginBase
*
* @ingroup views_join_handlers
*
* @Annotation
*/
class ViewsJoin extends ViewsHandlerAnnotationBase {
}

View File

@ -0,0 +1,85 @@
<?php
namespace Drupal\views\Annotation;
/**
* Defines a Plugin annotation object for views pager plugins.
*
* @see \Drupal\views\Plugin\views\pager\PagerPluginBase
*
* @ingroup views_pager_plugins
*
* @Annotation
*/
class ViewsPager extends ViewsPluginAnnotationBase {
/**
* The plugin ID.
*
* @var string
*/
public $id;
/**
* The plugin title used in the views UI.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $title = '';
/**
* An optional short title used in the views UI.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $short_title = '';
/**
* A short help string; this is displayed in the views UI.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $help = '';
/**
* The theme function used to render the pager's output.
*
* @var string
*/
public $theme;
/**
* The types of the display this plugin can be used with.
*
* For example the Feed display defines the type 'feed', so only rss style
* and row plugins can be used in the views UI.
*
* @var array
*/
public $display_types;
/**
* The base tables on which this pager plugin can be used.
*
* If no base table is specified the plugin can be used with all tables.
*
* @var array
*/
public $base;
/**
* Whether the plugin should be not selectable in the UI.
*
* If it's set to TRUE, you can still use it via the API in config files.
*
* @var bool
*/
public $no_ui;
}

View File

@ -0,0 +1,21 @@
<?php
namespace Drupal\views\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines an abstract base class for all views plugin annotations.
*/
abstract class ViewsPluginAnnotationBase extends Plugin {
/**
* Whether or not to register a theme function automatically.
*
* This property is optional and it does not need to be declared.
*
* @var bool
*/
public $register_theme = TRUE;
}

View File

@ -0,0 +1,59 @@
<?php
namespace Drupal\views\Annotation;
/**
* Defines a Plugin annotation object for views query plugins.
*
* @see \Drupal\views\Plugin\views\query\QueryPluginBase
*
* @ingroup views_query_plugins
*
* @Annotation
*/
class ViewsQuery extends ViewsPluginAnnotationBase {
/**
* The plugin ID.
*
* @var string
*/
public $id;
/**
* The plugin title used in the views UI.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $title = '';
/**
* An optional short title used in the views UI.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $short_title = '';
/**
* A short help string; this is displayed in the views UI.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $help = '';
/**
* Whether the plugin should be not selectable in the UI.
*
* If it's set to TRUE, you can still use it via the API in config files.
*
* @var bool
*/
public $no_ui;
}

View File

@ -0,0 +1,16 @@
<?php
namespace Drupal\views\Annotation;
/**
* Defines a Plugin annotation object for views relationship handlers.
*
* @see \Drupal\views\Plugin\views\relationship\RelationshipPluginBase
*
* @ingroup views_relationship_handlers
*
* @Annotation
*/
class ViewsRelationship extends ViewsHandlerAnnotationBase {
}

View File

@ -0,0 +1,83 @@
<?php
namespace Drupal\views\Annotation;
/**
* Defines a Plugin annotation object for views row plugins.
*
* @see \Drupal\views\Plugin\views\row\RowPluginBase
*
* @ingroup views_row_plugins
*
* @Annotation
*/
class ViewsRow extends ViewsPluginAnnotationBase {
/**
* The plugin ID.
*
* @var string
*/
public $id;
/**
* The plugin title used in the views UI.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $title = '';
/**
* An optional short title used in the views UI.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $short_title = '';
/**
* A short help string; this is displayed in the views UI.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $help = '';
/**
* The theme function used to render the row output.
*
* @var string
*/
public $theme;
/**
* The base tables on which this row plugin can be used.
*
* @var array
*/
public $base;
/**
* The types of the display this plugin can be used with.
*
* For example the Feed display defines the type 'feed', so only rss style
* and row plugins can be used in the views UI.
*
* @var array
*/
public $display_types;
/**
* Whether the plugin should be not selectable in the UI.
*
* If it's set to TRUE, you can still use it via the API in config files.
*
* @var bool
*/
public $no_ui;
}

View File

@ -0,0 +1,16 @@
<?php
namespace Drupal\views\Annotation;
/**
* Defines a Plugin annotation object for views sort handlers.
*
* @see \Drupal\views\Plugin\views\sort\SortPluginBase
*
* @ingroup views_sort_handlers
*
* @Annotation
*/
class ViewsSort extends ViewsHandlerAnnotationBase {
}

View File

@ -0,0 +1,85 @@
<?php
namespace Drupal\views\Annotation;
/**
* Defines a Plugin annotation object for views style plugins.
*
* @see \Drupal\views\Plugin\views\style\StylePluginBase
*
* @ingroup views_style_plugins
*
* @Annotation
*/
class ViewsStyle extends ViewsPluginAnnotationBase {
/**
* The plugin ID.
*
* @var string
*/
public $id;
/**
* The plugin title used in the views UI.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $title = '';
/**
* An optional short title used in the views UI.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $short_title = '';
/**
* A short help string; this is displayed in the views UI.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $help = '';
/**
* The theme function used to render the style output.
*
* @var string
*/
public $theme;
/**
* The types of the display this plugin can be used with.
*
* For example the Feed display defines the type 'feed', so only rss style
* and row plugins can be used in the views UI.
*
* @var array
*/
public $display_types;
/**
* The base tables on which this style plugin can be used.
*
* If no base table is specified the plugin can be used with all tables.
*
* @var array
*/
public $base;
/**
* Whether the plugin should be not selectable in the UI.
*
* If it's set to TRUE, you can still use it via the API in config files.
*
* @var bool
*/
public $no_ui;
}

View File

@ -0,0 +1,49 @@
<?php
namespace Drupal\views\Annotation;
/**
* Defines a Plugin annotation object for views wizard plugins.
*
* @see \Drupal\views\Plugin\views\wizard\WizardPluginBase
* @see \Drupal\views\Plugin\views\wizard\WizardInterface
*
* @ingroup views_wizard_plugins
*
* @Annotation
*/
class ViewsWizard extends ViewsPluginAnnotationBase {
/**
* The plugin ID.
*
* @var string
*/
public $id;
/**
* The plugin title used in the views UI.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $title = '';
/**
* An optional short title used in the views UI.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $short_title = '';
/**
* The base tables on which this wizard is used.
*
* @var array
*/
public $base_table;
}

View File

@ -0,0 +1,54 @@
<?php
namespace Drupal\views\Attribute;
use Drupal\Component\Plugin\Attribute\Plugin;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Defines a views access plugins type attribute for plugin discovery.
*
* @see \Drupal\views\Plugin\views\access\AccessPluginBase
*
* @ingroup views_access_plugins
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
class ViewsAccess extends Plugin {
/**
* Constructs a ViewsAccess attribute.
*
* @param string $id
* The plugin ID.
* @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $title
* The plugin title used in the views UI.
* @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $short_title
* (optional) The short title used in the views UI.
* @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $help
* (optional) A short help string; this is displayed in the views UI.
* @param string[]|null $display_types
* (optional) The types of the display this plugin can be used with.
* For example the Feed display defines the type 'feed', so only rss style
* and row plugins can be used in the views UI.
* @param string[] $base
* (optional) The base tables on which this access plugin can be used.
* If no base table is specified the plugin can be used with all tables.
* @param bool $no_ui
* (optional) Whether the plugin should be not selectable in the UI.
* If set to TRUE, you can still use it via the API in config files.
* Defaults to FALSE.
* @param class-string|null $deriver
* (optional) The deriver class.
*/
public function __construct(
public readonly string $id,
public readonly TranslatableMarkup $title,
public readonly ?TranslatableMarkup $short_title = NULL,
public readonly ?TranslatableMarkup $help = NULL,
public readonly ?array $display_types = NULL,
public readonly array $base = [],
public readonly bool $no_ui = FALSE,
public readonly ?string $deriver = NULL,
) {}
}

View File

@ -0,0 +1,17 @@
<?php
namespace Drupal\views\Attribute;
use Drupal\Component\Plugin\Attribute\PluginID;
/**
* Defines a Plugin attribute object for views area handlers.
*
* @see \Drupal\views\Plugin\views\area\AreaPluginBase
*
* @ingroup views_area_handlers
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
class ViewsArea extends PluginID {
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Drupal\views\Attribute;
use Drupal\Component\Plugin\Attribute\Plugin;
/**
* Defines a ViewsArgument attribute for plugin discovery.
*
* @see \Drupal\views\Plugin\views\argument\ArgumentPluginBase
*
* @ingroup views_argument_handlers
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
class ViewsArgument extends Plugin {
}

View File

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Drupal\views\Attribute;
use Drupal\Component\Plugin\Attribute\Plugin;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Defines a ViewsArgument attribute for plugin discovery.
*
* @see \Drupal\views\Plugin\views\argument_default\ArgumentDefaultPluginBase
*
* @ingroup views_argument_default_plugins
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
class ViewsArgumentDefault extends Plugin {
/**
* Constructs a ViewsArgument attribute.
*
* @param string $id
* The plugin ID.
* @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $title
* The plugin title used in the views UI.
* @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $short_title
* (optional) The short title used in the views UI.
* @param bool $no_ui
* (optional) Whether the plugin should be not selectable in the UI. If it's
* set to TRUE, you can still use it via the API in config files.
* @param class-string|null $deriver
* (optional) The deriver class.
*/
public function __construct(
public readonly string $id,
public readonly ?TranslatableMarkup $title = NULL,
public readonly ?TranslatableMarkup $short_title = NULL,
public readonly bool $no_ui = FALSE,
public readonly ?string $deriver = NULL,
) {}
}

View File

@ -0,0 +1,47 @@
<?php
namespace Drupal\views\Attribute;
use Drupal\Component\Plugin\Attribute\Plugin;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Defines a ViewsArgumentValidator attribute object for plugin discovery.
*
* Plugin Namespace: Plugin\ViewsArgumentValidator
*
* @see \Drupal\views\Plugin\views\argument_validator\ArgumentValidatorPluginBase
*
* @ingroup views_argument_validate_plugins
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
class ViewsArgumentValidator extends Plugin {
/**
* Constructs a ViewsArgumentValidator attribute.
*
* @param string $id
* The plugin ID.
* @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $title
* The plugin title used in the views UI.
* @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $short_title
* (optional) The short title used in the views UI.
* @param string|null $entity_type
* (optional) Entity type.
* @param bool $no_ui
* (optional) Whether the plugin should be not selectable in the UI.
* If set to TRUE, you can still use it via the API in config files.
* Defaults to FALSE.
* @param class-string|null $deriver
* (optional) The deriver class.
*/
public function __construct(
public readonly string $id,
public readonly ?TranslatableMarkup $title = NULL,
public readonly ?TranslatableMarkup $short_title = NULL,
public readonly ?string $entity_type = NULL,
public readonly bool $no_ui = FALSE,
public readonly ?string $deriver = NULL,
) {}
}

View File

@ -0,0 +1,54 @@
<?php
namespace Drupal\views\Attribute;
use Drupal\Component\Plugin\Attribute\Plugin;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Defines a views cache plugins type attribute for plugin discovery.
*
* @see \Drupal\views\Plugin\views\cache\CachePluginBase
*
* @ingroup views_cache_plugins
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
class ViewsCache extends Plugin {
/**
* Constructs a ViewsCache attribute.
*
* @param string $id
* The plugin ID.
* @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $title
* The plugin title used in the views UI.
* @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $short_title
* (optional) The short title used in the views UI.
* @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $help
* (optional) A short help string; this is displayed in the views UI.
* @param string[]|null $display_types
* (optional) The types of the display this plugin can be used with.
* For example the Feed display defines the type 'feed', so only rss style
* and row plugins can be used in the views UI.
* @param string[] $base
* (optional) The base tables on which this cache plugin can be used.
* If no base table is specified the plugin can be used with all tables.
* @param bool $no_ui
* (optional) Whether the plugin should be not selectable in the UI.
* If set to TRUE, you can still use it via the API in config files.
* Defaults to FALSE.
* @param class-string|null $deriver
* (optional) The deriver class.
*/
public function __construct(
public readonly string $id,
public readonly ?TranslatableMarkup $title = NULL,
public readonly ?TranslatableMarkup $short_title = NULL,
public readonly ?TranslatableMarkup $help = NULL,
public readonly ?array $display_types = NULL,
public readonly array $base = [],
public readonly ?bool $no_ui = NULL,
public readonly ?string $deriver = NULL,
) {}
}

View File

@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace Drupal\views\Attribute;
use Drupal\Component\Plugin\Attribute\Plugin;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Defines a Plugin attribute object for views display plugins.
*
* @see \Drupal\views\Plugin\views\display\DisplayPluginBase
*
* @ingroup views_display_plugins
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
class ViewsDisplay extends Plugin {
/**
* Constructs a views display attribute object.
*
* @param string $id
* The plugin ID.
* @param \Drupal\Core\StringTranslation\TranslatableMarkup $title
* The plugin title used in the views UI.
* @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $short_title
* (optional) The short title used in the views UI.
* @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $admin
* (optional) The administrative name of the display.
* The name is displayed on the Views overview and also used as default name
* for new displays.
* @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $help
* (optional) A short help string; this is displayed in the views UI.
* @param bool $uses_menu_links
* (optional) Whether or not to use hook_menu() to register a route.
* Defaults to FALSE.
* @param bool $uses_route
* (optional) Does the display plugin registers routes to the route.
* Defaults to FALSE.
* @param bool $uses_hook_block
* (optional) Does the display plugin provide blocks. Defaults to FALSE.
* @param bool $returns_response
* (optional) Whether the display returns a response object.
* Defaults to FALSE.
* @param string[]|null $contextual_links_locations
* (optional) A list of places where contextual links should be added.
* If you don't specify it there will be contextual links rendered for all
* displays of a view. If this is not set or regions have been specified,
* views will display an option to 'hide contextual links'. Use an empty
* array to disable.
* @param string[] $base
* (optional) The base tables on which this exposed form plugin can be used.
* If no base table is specified the plugin can be used with all tables.
* @param string|null $theme
* (optional) The theme function used to render the style output.
* @param bool $no_ui
* (optional) Whether the plugin should be not selectable in the UI.
* If it's set to TRUE, you can still use it via the API in config files.
* Defaults to FALSE.
* @param bool $register_theme
* (optional) Whether to register a theme function automatically. Defaults
* to TRUE.
* @param bool $entity_reference_display
* (optional) Custom property, used with
* \Drupal\views\Views::getApplicableViews(). Defaults to FALSE.
* @param class-string|null $deriver
* (optional) The deriver class.
*/
public function __construct(
public readonly string $id,
public readonly TranslatableMarkup $title,
public readonly ?TranslatableMarkup $short_title = NULL,
public readonly ?TranslatableMarkup $admin = NULL,
public readonly ?TranslatableMarkup $help = NULL,
public readonly bool $uses_menu_links = FALSE,
public readonly bool $uses_route = FALSE,
public readonly bool $uses_hook_block = FALSE,
public readonly bool $returns_response = FALSE,
public readonly ?array $contextual_links_locations = NULL,
public readonly array $base = [],
public readonly ?string $theme = NULL,
public readonly bool $no_ui = FALSE,
public readonly bool $register_theme = TRUE,
public readonly bool $entity_reference_display = FALSE,
public readonly ?string $deriver = NULL,
) {}
}

View File

@ -0,0 +1,45 @@
<?php
namespace Drupal\views\Attribute;
use Drupal\Component\Plugin\Attribute\Plugin;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Defines a Plugin attribute object for views display extender plugins.
*
* @see \Drupal\views\Plugin\views\display_extender\DisplayExtenderPluginBase
*
* @ingroup views_display_extender_plugins
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
class ViewsDisplayExtender extends Plugin {
/**
* Constructs an ViewsDisplayExtender attribute.
*
* @param string $id
* The plugin ID.
* @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $title
* The plugin title used in the views UI.
* @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $short_title
* (optional) The short title used in the views UI.
* @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $help
* (optional) A short help string; this is displayed in the views UI.
* @param bool $no_ui
* (optional) Whether the plugin should be not selectable in the UI.
* If set to TRUE, you can still use it via the API in config files.
* Defaults to FALSE.
* @param class-string|null $deriver
* (optional) The deriver class.
*/
public function __construct(
public readonly string $id,
public readonly ?TranslatableMarkup $title = NULL,
public readonly ?TranslatableMarkup $short_title = NULL,
public readonly ?TranslatableMarkup $help = NULL,
public readonly bool $no_ui = FALSE,
public readonly ?string $deriver = NULL,
) {}
}

View File

@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace Drupal\views\Attribute;
use Drupal\Component\Plugin\Attribute\Plugin;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Defines a Plugin attribute object for views exposed form plugins.
*
* @see \Drupal\views\Plugin\views\exposed_form\ExposedFormPluginInterface
* @see \Drupal\views\Plugin\views\exposed_form\ExposedFormPluginBase
*
* @ingroup views_exposed_form_plugins
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
class ViewsExposedForm extends Plugin {
/**
* Constructs a views exposed form attribute object.
*
* @param string $id
* The plugin ID.
* @param \Drupal\Core\StringTranslation\TranslatableMarkup $title
* The plugin title used in the views UI.
* @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $short_title
* (optional) The short title used in the views UI.
* @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $help
* (optional) A short help string; this is displayed in the views UI.
* @param string[]|null $display_types
* (optional) The types of the display this plugin can be used with.
* For example the Feed display defines the type 'feed', so only rss style
* and row plugins can be used in the views UI.
* @param string[] $base
* (optional) The base tables on which this exposed form plugin can be used.
* If no base table is specified the plugin can be used with all tables.
* @param bool $no_ui
* (optional) Whether the plugin should be not selectable in the UI.
* If it's set to TRUE, you can still use it via the API in config files.
* Defaults to FALSE.
* @param bool $register_theme
* (optional) Whether to register a theme function automatically. Defaults
* to TRUE.
* @param class-string|null $deriver
* (optional) The deriver class.
*/
public function __construct(
public readonly string $id,
public readonly TranslatableMarkup $title,
public readonly ?TranslatableMarkup $short_title = NULL,
public readonly ?TranslatableMarkup $help = NULL,
public readonly ?array $display_types = NULL,
public readonly array $base = [],
public readonly bool $no_ui = FALSE,
public readonly bool $register_theme = TRUE,
public readonly ?string $deriver = NULL,
) {}
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Drupal\views\Attribute;
use Drupal\Component\Plugin\Attribute\PluginID;
/**
* Defines a Plugin attribute class for views field handlers.
*
* @see \Drupal\views\Plugin\views\field\FieldPluginBase
*
* @ingroup views_field_handlers
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
class ViewsField extends PluginID {
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Drupal\views\Attribute;
use Drupal\Component\Plugin\Attribute\PluginID;
/**
* Defines a Plugin attribute class for views filter handlers.
*
* @see \Drupal\views\Plugin\views\filter\FilterPluginBase
*
* @ingroup views_filter_handlers
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
class ViewsFilter extends PluginID {
}

View File

@ -0,0 +1,17 @@
<?php
namespace Drupal\views\Attribute;
use Drupal\Component\Plugin\Attribute\PluginID;
/**
* Defines a Plugin attribute object for views join plugins.
*
* @see \Drupal\views\Plugin\views\join\JoinPluginBase
*
* @ingroup views_join_handlers
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
class ViewsJoin extends PluginID {
}

View File

@ -0,0 +1,60 @@
<?php
namespace Drupal\views\Attribute;
use Drupal\Component\Plugin\Attribute\Plugin;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Defines a views pager plugins type attribute for plugin discovery.
*
* @see \Drupal\views\Plugin\views\pager\PagerPluginBase
*
* @ingroup views_pager_plugins
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
class ViewsPager extends Plugin {
/**
* Constructs a ViewsPager attribute.
*
* @param string $id
* The plugin ID.
* @param \Drupal\Core\StringTranslation\TranslatableMarkup $title
* The plugin title used in the views UI.
* @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $short_title
* (optional) The short title used in the views UI.
* @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $help
* (optional) A short help string; this is displayed in the views UI.
* @param string|null $theme
* (optional) The theme function used to render the pager's output.
* @param string[]|null $display_types
* (optional) The types of the display this plugin can be used with.
* For example the Feed display defines the type 'feed', so only rss style
* and row plugins can be used in the views UI.
* @param string[] $base
* (optional) The base tables on which this access plugin can be used.
* If no base table is specified the plugin can be used with all tables.
* @param bool $no_ui
* (optional) Whether the plugin should be not selectable in the UI.
* If set to TRUE, you can still use it via the API in config files.
* Defaults to FALSE.
* @param bool $register_theme
* (optional) Whether or not to register a theme function automatically.
* @param class-string|null $deriver
* (optional) The deriver class.
*/
public function __construct(
public readonly string $id,
public readonly TranslatableMarkup $title,
public readonly ?TranslatableMarkup $short_title = NULL,
public readonly ?TranslatableMarkup $help = NULL,
public readonly ?string $theme = NULL,
public readonly ?array $display_types = NULL,
public readonly array $base = [],
public readonly bool $no_ui = FALSE,
public readonly bool $register_theme = TRUE,
public readonly ?string $deriver = NULL,
) {}
}

View File

@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Drupal\views\Attribute;
use Drupal\Component\Plugin\Attribute\Plugin;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Defines a ViewsQuery attribute for plugin discovery.
*
* @see \Drupal\views\Plugin\views\query\QueryPluginBase
*
* @ingroup views_query_plugins
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
class ViewsQuery extends Plugin {
/**
* Constructs an ViewsDisplayExtender attribute.
*
* @param string $id
* The plugin ID.
* @param \Drupal\Core\StringTranslation\TranslatableMarkup $title
* The plugin title used in the views UI.
* @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $short_title
* (optional) The short title used in the views UI.
* @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $help
* (optional) A short help string; this is displayed in the views UI.
* @param bool $no_ui
* (optional) Whether the plugin should be not selectable in the UI.
* If set to TRUE, you can still use it via the API in config files.
* Defaults to FALSE.
* @param class-string|null $deriver
* (optional) The deriver class.
*/
public function __construct(
public readonly string $id,
public readonly ?TranslatableMarkup $title,
public readonly ?TranslatableMarkup $short_title = NULL,
public readonly ?TranslatableMarkup $help = NULL,
public readonly bool $no_ui = FALSE,
public readonly ?string $deriver = NULL,
) {}
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Drupal\views\Attribute;
use Drupal\Component\Plugin\Attribute\PluginID;
/**
* Defines a Plugin attribute class for views relationship handlers.
*
* @see \Drupal\views\Plugin\views\relationship\RelationshipPluginBase
*
* @ingroup views_relationship_handlers
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
class ViewsRelationship extends PluginID {
}

View File

@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace Drupal\views\Attribute;
use Drupal\Component\Plugin\Attribute\Plugin;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Defines a ViewsRow attribute for plugin discovery.
*
* @see \Drupal\views\Plugin\views\style\StylePluginBase
*
* @ingroup views_row_plugins
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
class ViewsRow extends Plugin {
/**
* Constructs an ViewsRow attribute.
*
* @param string $id
* The plugin ID.
* @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $title
* (optional) The plugin title used in the views UI.
* @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $short_title
* (optional) The short title used in the views UI.
* @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $help
* (optional) A short help string; this is displayed in the views UI.
* @param string[] $display_types
* (optional) The types of the display this plugin can be used with.
* For example the Feed display defines the type 'feed', so only rss style
* and row plugins can be used in the views UI.
* @param string[] $base
* (optional) The base tables on which this style plugin can be used.
* If no base table is specified the plugin can be used with all tables.
* @param string|null $theme
* (optional) The theme function used to render the style output.
* @param bool $no_ui
* (optional) Whether the plugin should be not selectable in the UI.
* If set to TRUE, you can still use it via the API in config files.
* Defaults to FALSE.
* @param bool $register_theme
* (optional) Whether to register a theme function automatically. Defaults
* to TRUE.
* @param class-string|null $deriver
* (optional) The deriver class.
*/
public function __construct(
public readonly string $id,
public readonly ?TranslatableMarkup $title = NULL,
public readonly ?TranslatableMarkup $short_title = NULL,
public readonly ?TranslatableMarkup $help = NULL,
public readonly array $display_types = [],
public readonly array $base = [],
public readonly ?string $theme = NULL,
public readonly bool $no_ui = FALSE,
public readonly bool $register_theme = TRUE,
public readonly ?string $deriver = NULL,
) {}
}

View File

@ -0,0 +1,17 @@
<?php
namespace Drupal\views\Attribute;
use Drupal\Component\Plugin\Attribute\PluginID;
/**
* Defines a Plugin attribute object for views sort handlers.
*
* @see \Drupal\views\Plugin\views\sort\SortPluginBase
*
* @ingroup views_sort_handlers
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
class ViewsSort extends PluginID {
}

View File

@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace Drupal\views\Attribute;
use Drupal\Component\Plugin\Attribute\Plugin;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Defines a views style plugins type attribute for plugin discovery.
*
* @see \Drupal\views\Plugin\views\style\StylePluginBase
*
* @ingroup views_style_plugins
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
class ViewsStyle extends Plugin {
/**
* Constructs a ViewsStyle attribute.
*
* @param string $id
* The plugin ID.
* @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $title
* The plugin title used in the views UI.
* @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $short_title
* (optional) The short title used in the views UI.
* @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $help
* (optional) A short help string; this is displayed in the views UI.
* @param string|null $theme
* (optional) The theme function used to render the style output.
* @param string[] $display_types
* The types of the display this plugin can be used with.
* For example the Feed display defines the type 'feed', so only rss style
* and row plugins can be used in the views UI.
* @param string[] $base
* (optional) The base tables on which this access plugin can be used.
* If no base table is specified the plugin can be used with all tables.
* @param bool $no_ui
* (optional) Whether the plugin should be not selectable in the UI.
* If set to TRUE, you can still use it via the API in config files.
* Defaults to FALSE.
* @param bool $register_theme
* (optional) Whether or not to register a theme function automatically.
* @param class-string|null $deriver
* (optional) The deriver class.
*/
public function __construct(
public readonly string $id,
public readonly TranslatableMarkup $title,
public readonly ?TranslatableMarkup $short_title = NULL,
public readonly ?TranslatableMarkup $help = NULL,
public readonly ?string $theme = NULL,
public readonly array $display_types = [],
public readonly array $base = [],
public readonly bool $no_ui = FALSE,
public readonly bool $register_theme = TRUE,
public readonly ?string $deriver = NULL,
) {}
}

View File

@ -0,0 +1,39 @@
<?php
namespace Drupal\views\Attribute;
use Drupal\Component\Plugin\Attribute\Plugin;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Defines a Plugin attribute object for views wizard plugins.
*
* @see \Drupal\views\Plugin\views\wizard\WizardPluginBase
* @see \Drupal\views\Plugin\views\wizard\WizardInterface
*
* @ingroup views_wizard_plugins
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
class ViewsWizard extends Plugin {
/**
* Constructs an ViewsWizard attribute.
*
* @param string $id
* The plugin ID.
* @param \Drupal\Core\StringTranslation\TranslatableMarkup $title
* The plugin title used in the views UI.
* @param string|null $base_table
* (optional) The base table on which this wizard is used. The base_table is
* required when a deriver class is not defined.
* @param class-string|null $deriver
* (optional) The deriver class.
*/
public function __construct(
public readonly string $id,
public readonly TranslatableMarkup $title,
public readonly ?string $base_table = NULL,
public readonly ?string $deriver = NULL,
) {}
}

View File

@ -0,0 +1,232 @@
<?php
namespace Drupal\views\Controller;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Ajax\PrependCommand;
use Drupal\Core\Ajax\ReplaceCommand;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Path\CurrentPathStack;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Routing\RedirectDestinationInterface;
use Drupal\Core\Ajax\ScrollTopCommand;
use Drupal\views\Ajax\ViewAjaxResponse;
use Drupal\views\ViewExecutableFactory;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
/**
* Defines a controller to load a view via AJAX.
*/
class ViewAjaxController implements ContainerInjectionInterface {
/**
* Parameters that should be filtered and ignored inside ajax requests.
*/
public const FILTERED_QUERY_PARAMETERS = [
'view_name',
'view_display_id',
'view_args',
'view_path',
'view_dom_id',
'pager_element',
'view_base_path',
'ajax_page_state',
'_drupal_ajax',
FormBuilderInterface::AJAX_FORM_REQUEST,
MainContentViewSubscriber::WRAPPER_FORMAT,
];
/**
* The entity storage for views.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $storage;
/**
* The factory to load a view executable with.
*
* @var \Drupal\views\ViewExecutableFactory
*/
protected $executableFactory;
/**
* The renderer.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* The current path.
*
* @var \Drupal\Core\Path\CurrentPathStack
*/
protected $currentPath;
/**
* The redirect destination.
*
* @var \Drupal\Core\Routing\RedirectDestinationInterface
*/
protected $redirectDestination;
/**
* Constructs a ViewAjaxController object.
*
* @param \Drupal\Core\Entity\EntityStorageInterface $storage
* The entity storage for views.
* @param \Drupal\views\ViewExecutableFactory $executable_factory
* The factory to load a view executable with.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer.
* @param \Drupal\Core\Path\CurrentPathStack $current_path
* The current path.
* @param \Drupal\Core\Routing\RedirectDestinationInterface $redirect_destination
* The redirect destination.
*/
public function __construct(EntityStorageInterface $storage, ViewExecutableFactory $executable_factory, RendererInterface $renderer, CurrentPathStack $current_path, RedirectDestinationInterface $redirect_destination) {
$this->storage = $storage;
$this->executableFactory = $executable_factory;
$this->renderer = $renderer;
$this->currentPath = $current_path;
$this->redirectDestination = $redirect_destination;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager')->getStorage('view'),
$container->get('views.executable'),
$container->get('renderer'),
$container->get('path.current'),
$container->get('redirect.destination')
);
}
/**
* Loads and renders a view via AJAX.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request object.
*
* @return \Drupal\views\Ajax\ViewAjaxResponse
* The view response as ajax response.
*
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
* Thrown when the view was not found.
*/
public function ajaxView(Request $request) {
$name = $request->get('view_name');
$display_id = $request->get('view_display_id');
if (isset($name) && isset($display_id)) {
$args = $request->get('view_args', '');
$args = $args !== '' ? explode('/', Html::decodeEntities($args)) : [];
// Arguments can be empty, make sure they are passed on as NULL so that
// argument validation is not triggered.
$args = array_map(function ($arg) {
return ($arg == '' ? NULL : $arg);
}, $args);
$path = $request->get('view_path');
$dom_id = $request->get('view_dom_id');
$dom_id = isset($dom_id) ? preg_replace('/[^a-zA-Z0-9_-]+/', '-', $dom_id) : NULL;
$pager_element = $request->get('pager_element');
$pager_element = isset($pager_element) ? intval($pager_element) : NULL;
$response = new ViewAjaxResponse();
// Remove all of this stuff from the query of the request so it doesn't
// end up in pagers and tablesort URLs. Additionally we need to preserve
// ajax_page_state and add it back after the request has been processed so
// the related listener can behave correctly.
// @todo Remove this parsing once these are removed from the request in
// https://www.drupal.org/node/2504709.
$existing_page_state = $request->get('ajax_page_state');
foreach (self::FILTERED_QUERY_PARAMETERS as $key) {
$request->query->remove($key);
$request->request->remove($key);
}
// Load the view.
if (!$entity = $this->storage->load($name)) {
throw new NotFoundHttpException();
}
$view = $this->executableFactory->get($entity);
if ($view && $view->access($display_id) && $view->setDisplay($display_id) && $view->display_handler->ajaxEnabled()) {
$response->setView($view);
// Fix the current path for paging.
if (!empty($path)) {
$this->currentPath->setPath('/' . ltrim($path, '/'), $request);
}
// Create a clone of the request object to avoid mutating the request
// object stored in the request stack.
$request_clone = clone $request;
// Add all POST data, because AJAX is sometimes a POST and many things,
// such as tablesorts, exposed filters and paging assume GET.
$param_union = $request_clone->request->all() + $request_clone->query->all();
$request_clone->query->replace($param_union);
// Overwrite the destination.
// @see the redirect.destination service.
$origin_destination = $request_clone->getBasePath() . '/' . ltrim($path ?? '/', '/');
$used_query_parameters = $request_clone->query->all();
$query = UrlHelper::buildQuery($used_query_parameters);
if ($query != '') {
$origin_destination .= '?' . $query;
}
$this->redirectDestination->set($origin_destination);
// Override the display's pager_element with the one actually used.
if (isset($pager_element)) {
$response->addCommand(new ScrollTopCommand(".js-view-dom-id-$dom_id"));
$view->displayHandlers->get($display_id)->setOption('pager_element', $pager_element);
}
// Reuse the same DOM id so it matches that in drupalSettings.
$view->dom_id = $dom_id;
// Populate request attributes temporarily with ajax_page_state theme
// and theme_token for theme negotiation.
$theme_keys = [
'theme' => TRUE,
'theme_token' => TRUE,
];
if (is_array($existing_page_state) &&
($temp_attributes = array_intersect_key($existing_page_state, $theme_keys))) {
$request->attributes->set('ajax_page_state', $temp_attributes);
}
$preview = $view->preview($display_id, $args);
$request->attributes->remove('ajax_page_state');
$response->addCommand(new ReplaceCommand(".js-view-dom-id-$dom_id", $preview));
$response->addCommand(new PrependCommand(".js-view-dom-id-$dom_id", ['#type' => 'status_messages']));
$request->query->set('ajax_page_state', $existing_page_state);
if (!empty($preview['#attached'])) {
$response->setAttachments($preview['#attached']);
}
return $response;
}
else {
throw new AccessDeniedHttpException();
}
}
else {
throw new NotFoundHttpException();
}
}
}

View File

@ -0,0 +1,115 @@
<?php
namespace Drupal\views;
use Drupal\Component\Plugin\Exception\PluginException;
use Drupal\Component\Plugin\PluginManagerInterface;
use Drupal\Core\Plugin\DefaultLazyPluginCollection;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\views\Plugin\views\display\DisplayPluginInterface;
/**
* A class which wraps the displays of a view so you can lazy-initialize them.
*/
class DisplayPluginCollection extends DefaultLazyPluginCollection {
use StringTranslationTrait;
/**
* Stores a reference to the view which has this displays attached.
*
* @var \Drupal\views\ViewExecutable
*/
protected $view;
/**
* {@inheritdoc}
*/
protected $pluginKey = 'display_plugin';
/**
* Constructs a DisplayPluginCollection object.
*
* @param \Drupal\views\ViewExecutable $view
* The view which has this displays attached.
* @param \Drupal\Component\Plugin\PluginManagerInterface $manager
* The manager to be used for instantiating plugins.
*/
public function __construct(ViewExecutable $view, PluginManagerInterface $manager) {
parent::__construct($manager, $view->storage->get('display'));
$this->view = $view;
$this->initializePlugin('default');
}
/**
* Destructs a DisplayPluginCollection object.
*/
public function __destruct() {
$this->clear();
}
/**
* {@inheritdoc}
*
* @return \Drupal\views\Plugin\views\display\DisplayPluginBase
* The display plugin.
*/
public function &get($instance_id) {
return parent::get($instance_id);
}
/**
* {@inheritdoc}
*/
public function clear() {
foreach (array_filter($this->pluginInstances) as $display) {
if ($display instanceof DisplayPluginInterface) {
$display->destroy();
}
}
parent::clear();
}
/**
* {@inheritdoc}
*/
protected function initializePlugin($display_id) {
// Retrieve and initialize the new display handler with data.
$display = &$this->view->storage->getDisplay($display_id);
try {
$this->configurations[$display_id] = $display;
parent::initializePlugin($display_id);
}
// Catch any plugin exceptions that are thrown. So we can fail nicely if a
// display plugin isn't found.
catch (PluginException $e) {
$message = $e->getMessage();
\Drupal::messenger()->addWarning($this->t('@message', ['@message' => $message]));
}
// If no plugin instance has been created, return NULL.
if (empty($this->pluginInstances[$display_id])) {
return NULL;
}
$this->pluginInstances[$display_id]->initDisplay($this->view, $display);
// If this is not the default display handler, let it know which is since
// it may well use some data from the default.
if ($display_id != 'default') {
$this->pluginInstances[$display_id]->default_display = $this->pluginInstances['default'];
}
}
/**
* {@inheritdoc}
*/
public function remove($instance_id) {
$this->get($instance_id)->remove();
parent::remove($instance_id);
}
}

View File

@ -0,0 +1,104 @@
<?php
namespace Drupal\views\Element;
use Drupal\Core\Render\Attribute\RenderElement;
use Drupal\Core\Render\Element\RenderElementBase;
use Drupal\views\Exception\ViewRenderElementException;
use Drupal\views\Views;
/**
* Provides a render element to display a view.
*/
#[RenderElement('view')]
class View extends RenderElementBase {
/**
* {@inheritdoc}
*/
public function getInfo() {
return [
'#pre_render' => [
[static::class, 'preRenderViewElement'],
],
'#name' => NULL,
'#display_id' => 'default',
'#arguments' => [],
'#embed' => TRUE,
'#cache' => [],
];
}
/**
* View element pre render callback.
*/
public static function preRenderViewElement($element) {
// Allow specific Views displays to explicitly perform pre-rendering, for
// those displays that need to be able to know the fully built render array.
if (!empty($element['#pre_rendered'])) {
return $element;
}
if (!isset($element['#view'])) {
$view = Views::getView($element['#name']);
if (!$view) {
throw new ViewRenderElementException("Invalid View name ({$element['#name']}) given.");
}
}
else {
$view = $element['#view'];
}
$element += $view->element;
$view->element = &$element;
// Mark the element as being prerendered, so other code like
// \Drupal\views\ViewExecutable::setCurrentPage knows that its no longer
// possible to manipulate the $element.
$view->element['#pre_rendered'] = TRUE;
if (isset($element['#response'])) {
$view->setResponse($element['#response']);
}
if ($view && $view->access($element['#display_id'])) {
if (!empty($element['#embed'])) {
$element['view_build'] = $view->preview($element['#display_id'], $element['#arguments']);
}
else {
// Add contextual links to the view. We need to attach them to the dummy
// $view_array variable, since contextual_preprocess() requires that
// they be attached to an array (not an object) in order to process
// them. For our purposes, it doesn't matter what we attach them to,
// since once they are processed by contextual_preprocess() they will
// appear in the $title_suffix variable (which we will then render in
// views-view.html.twig).
$view->setDisplay($element['#display_id']);
// Add the result of the executed view as a child element so any
// #pre_render elements for the view will get processed. A #pre_render
// element cannot be added to the main element as this is already inside
// a #pre_render callback.
$element['view_build'] = $view->executeDisplay($element['#display_id'], $element['#arguments']);
if (isset($element['view_build']['#title'])) {
$element['#title'] = &$element['view_build']['#title'];
}
if (empty($view->display_handler->getPluginDefinition()['returns_response'])) {
// views_add_contextual_links() needs the following information in
// order to be attached to the view.
$element['#view_id'] = $view->storage->id();
$element['#view_display_show_admin_links'] = $view->getShowAdminLinks();
$element['#view_display_plugin_id'] = $view->display_handler->getPluginId();
views_add_contextual_links($element, 'view', $view->current_display);
}
}
if (empty($view->display_handler->getPluginDefinition()['returns_response'])) {
$element['#attributes']['class'][] = 'views-element-container';
$element['#theme_wrappers'] = ['container'];
}
}
return $element;
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace Drupal\views\Entity\Render;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\views\ResultRow;
use Drupal\views\ViewExecutable;
/**
* Renders entities in a configured language.
*/
class ConfigurableLanguageRenderer extends EntityTranslationRendererBase {
/**
* A specific language code for rendering if available.
*
* @var string|null
*/
protected $langcode;
/**
* Constructs a renderer object.
*
* @param \Drupal\views\ViewExecutable $view
* The entity row being rendered.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
* @param string|null $langcode
* A specific language code to set, if available.
*/
public function __construct(ViewExecutable $view, LanguageManagerInterface $language_manager, EntityTypeInterface $entity_type, $langcode) {
parent::__construct($view, $language_manager, $entity_type);
$this->langcode = $langcode;
}
/**
* {@inheritdoc}
*/
public function getLangcode(ResultRow $row) {
return $this->langcode;
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace Drupal\views\Entity\Render;
use Drupal\views\ResultRow;
/**
* Renders entities in their default language.
*/
class DefaultLanguageRenderer extends EntityTranslationRendererBase {
/**
* {@inheritdoc}
*/
public function getLangcode(ResultRow $row) {
return $row->_entity->getUntranslated()->language()->getId();
}
/**
* {@inheritdoc}
*/
public function getLangcodeByRelationship(ResultRow $row, string $relationship = 'none'): string {
$entity = $this->getEntity($row, $relationship);
return $entity->getUntranslated()->language()->getId();
}
}

View File

@ -0,0 +1,288 @@
<?php
namespace Drupal\views\Entity\Render;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\views\Plugin\views\field\EntityField;
use Drupal\views\Plugin\views\query\QueryPluginBase;
use Drupal\views\ResultRow;
use Drupal\views\ViewExecutable;
/**
* Renders entity fields.
*
* This is used to build render arrays for all entity field values of a view
* result set sharing the same relationship. An entity translation renderer is
* used internally to handle entity language properly.
*/
class EntityFieldRenderer extends RendererBase {
use EntityTranslationRenderTrait;
use DependencySerializationTrait;
/**
* The relationship being handled.
*
* @var string
*/
protected $relationship;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The entity repository service.
*
* @var \Drupal\Core\Entity\EntityRepositoryInterface
*/
protected $entityRepository;
/**
* A list of indexes of rows whose fields have already been rendered.
*
* @var int[]
*/
protected $processedRows = [];
/**
* Constructs an EntityFieldRenderer object.
*
* @param \Drupal\views\ViewExecutable $view
* The view whose fields are being rendered.
* @param string $relationship
* The relationship to be handled.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
* The entity repository.
*/
public function __construct(ViewExecutable $view, $relationship, LanguageManagerInterface $language_manager, EntityTypeInterface $entity_type, EntityTypeManagerInterface $entity_type_manager, EntityRepositoryInterface $entity_repository) {
parent::__construct($view, $language_manager, $entity_type);
$this->relationship = $relationship;
$this->entityTypeManager = $entity_type_manager;
$this->entityRepository = $entity_repository;
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
return $this->getEntityTranslationRenderer()->getCacheContexts();
}
/**
* {@inheritdoc}
*/
public function getEntityTypeId() {
return $this->entityType->id();
}
/**
* {@inheritdoc}
*/
protected function getEntityTypeManager() {
return $this->entityTypeManager;
}
/**
* {@inheritdoc}
*/
protected function getEntityRepository() {
return $this->entityRepository;
}
/**
* {@inheritdoc}
*/
protected function getLanguageManager() {
return $this->languageManager;
}
/**
* {@inheritdoc}
*/
protected function getView() {
return $this->view;
}
/**
* {@inheritdoc}
*/
public function query(QueryPluginBase $query, $relationship = NULL) {
$this->getEntityTranslationRenderer()->query($query, $relationship);
}
/**
* Renders entity field data.
*
* @param \Drupal\views\ResultRow $row
* A single row of the query result.
* @param \Drupal\views\Plugin\views\field\EntityField $field
* (optional) A field to be rendered.
*
* @return array
* A renderable array for the entity data contained in the result row.
*/
public function render(ResultRow $row, ?EntityField $field = NULL) {
// The method is called for each field in each result row. In order to
// leverage multiple-entity building of formatter output, we build the
// render arrays for all fields in all rows on the first call.
if (!isset($this->build)) {
$this->build = $this->buildFields($this->view->result);
}
if (isset($field)) {
$field_id = $field->options['id'];
// Pick the render array for the row / field we are being asked to render,
// and remove it from $this->build to free memory as we progress.
if (isset($this->build[$row->index][$field_id])) {
$build = $this->build[$row->index][$field_id];
unset($this->build[$row->index][$field_id]);
}
elseif (isset($this->build[$row->index])) {
// In the uncommon case where a field gets rendered several times
// (typically through direct Views API calls), the pre-computed render
// array was removed by the unset() above. We have to manually rebuild
// the render array for the row.
$build = $this->buildFields([$row])[$row->index][$field_id];
}
else {
// In case the relationship is optional, there might not be any fields
// to render for this row.
$build = [];
}
}
else {
// Same logic as above, in the case where we are being called for a whole
// row.
if (isset($this->build[$row->index])) {
$build = $this->build[$row->index];
unset($this->build[$row->index]);
}
else {
$build = $this->buildFields([$row])[$row->index];
}
}
return $build;
}
/**
* Builds the render arrays for all fields of all result rows.
*
* The output is built using EntityViewDisplay objects to leverage
* multiple-entity building and ensure a common code path with regular entity
* view.
* - Each relationship is handled by a separate EntityFieldRenderer instance,
* since it operates on its own set of entities. This also ensures different
* entity types are handled separately, as they imply different
* relationships.
* - Within each relationship, the fields to render are arranged in unique
* sets containing each field at most once (an EntityViewDisplay can
* only process a field once with given display options, but a View can
* contain the same field several times with different display options).
* - For each set of fields, entities are processed by bundle, so that
* formatters can operate on the proper field definition for the bundle.
*
* @param \Drupal\views\ResultRow[] $values
* An array of all ResultRow objects returned from the query.
*
* @return array
* A renderable array for the fields handled by this renderer.
*
* @see \Drupal\Core\Entity\Entity\EntityViewDisplay
*/
protected function buildFields(array $values) {
$build = [];
if ($values && ($field_ids = $this->getRenderableFieldIds())) {
$entity_type_id = $this->getEntityTypeId();
// Collect the entities for the relationship, fetch the right translation,
// and group by bundle. For each result row, the corresponding entity can
// be obtained from any of the fields handlers, so we arbitrarily use the
// first one.
$entities_by_bundles = [];
$field = $this->view->field[current($field_ids)];
foreach ($values as $result_row) {
if ($entity = $field->getEntity($result_row)) {
$relationship = $field->options['relationship'] ?? 'none';
$entities_by_bundles[$entity->bundle()][$result_row->index] = $this->getEntityTranslationByRelationship($entity, $result_row, $relationship);
}
}
// Determine unique sets of fields that can be processed by the same
// display. Fields that appear several times in the View open additional
// "overflow" displays.
$display_sets = [];
foreach ($field_ids as $field_id) {
$field = $this->view->field[$field_id];
$field_name = $field->definition['field_name'];
$index = 0;
while (isset($display_sets[$index]['field_names'][$field_name])) {
$index++;
}
$display_sets[$index]['field_names'][$field_name] = $field;
$display_sets[$index]['field_ids'][$field_id] = $field;
}
// For each set of fields, build the output by bundle.
foreach ($display_sets as $display_fields) {
foreach ($entities_by_bundles as $bundle => $bundle_entities) {
// Create the display, and configure the field display options.
$display = EntityViewDisplay::create([
'targetEntityType' => $entity_type_id,
'bundle' => $bundle,
'status' => TRUE,
]);
foreach ($display_fields['field_ids'] as $field) {
$display->setComponent($field->definition['field_name'], [
'type' => $field->options['type'],
'settings' => $field->options['settings'],
]);
}
// Let the display build the render array for the entities.
$display_build = $display->buildMultiple($bundle_entities);
// Collect the field render arrays and index them using our internal
// row indexes and field IDs.
foreach ($display_build as $row_index => $entity_build) {
foreach ($display_fields['field_ids'] as $field_id => $field) {
$build[$row_index][$field_id] = !empty($entity_build[$field->definition['field_name']]) ? $entity_build[$field->definition['field_name']] : [];
}
}
}
}
}
return $build;
}
/**
* Returns a list of names of entity fields to be rendered.
*
* @return string[]
* An associative array of views fields.
*/
protected function getRenderableFieldIds() {
$field_ids = [];
foreach ($this->view->field as $field_id => $field) {
if ($field instanceof EntityField && $field->relationship == $this->relationship) {
$field_ids[] = $field_id;
}
}
return $field_ids;
}
}

View File

@ -0,0 +1,106 @@
<?php
namespace Drupal\views\Entity\Render;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\TypedData\TranslatableInterface;
use Drupal\views\Plugin\views\PluginBase;
use Drupal\views\ResultRow;
/**
* Trait used to instantiate the view's entity translation renderer.
*/
trait EntityTranslationRenderTrait {
/**
* The renderer to be used to render the entity row.
*
* @var \Drupal\views\Entity\Render\EntityTranslationRendererBase
*/
protected $entityTranslationRenderer;
/**
* Returns the current renderer.
*
* @return \Drupal\views\Entity\Render\EntityTranslationRendererBase
* The configured renderer.
*/
protected function getEntityTranslationRenderer() {
if (!isset($this->entityTranslationRenderer)) {
$view = $this->getView();
$rendering_language = $view->display_handler->getOption('rendering_language');
$langcode = NULL;
$dynamic_renderers = [
'***LANGUAGE_entity_translation***' => 'TranslationLanguageRenderer',
'***LANGUAGE_entity_default***' => 'DefaultLanguageRenderer',
];
$entity_type = $this->getEntityTypeManager()->getDefinition($this->getEntityTypeId());
if (isset($dynamic_renderers[$rendering_language])) {
// Dynamic language set based on result rows or instance defaults.
$class = '\Drupal\views\Entity\Render\\' . $dynamic_renderers[$rendering_language];
$this->entityTranslationRenderer = new $class($view, $this->getLanguageManager(), $entity_type);
}
else {
if (str_contains($rendering_language, '***LANGUAGE_')) {
$langcode = PluginBase::queryLanguageSubstitutions()[$rendering_language];
}
else {
// Specific langcode set.
$langcode = $rendering_language;
}
$this->entityTranslationRenderer = new ConfigurableLanguageRenderer($view, $this->getLanguageManager(), $entity_type, $langcode);
}
}
return $this->entityTranslationRenderer;
}
/**
* Returns the entity translation matching the configured row language.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity object the field value being processed is attached to.
* @param \Drupal\views\ResultRow $row
* The result row the field value being processed belongs to.
* @param string $relationship
* The relationship to be used, or 'none' by default.
*
* @return \Drupal\Core\Entity\EntityInterface
* The entity translation object for the specified row.
*/
public function getEntityTranslationByRelationship(EntityInterface $entity, ResultRow $row, string $relationship = 'none'): EntityInterface {
// We assume the same language should be used for all entity fields
// belonging to a single row, even if they are attached to different entity
// types. Below we apply language fallback to ensure a valid value is always
// picked.
if ($entity instanceof TranslatableInterface && count($entity->getTranslationLanguages()) > 1) {
$langcode = $this->getEntityTranslationRenderer()->getLangcodeByRelationship($row, $relationship);
$translation = $this->getEntityRepository()->getTranslationFromContext($entity, $langcode);
}
return $translation ?? $entity;
}
/**
* Returns the entity type identifier.
*
* @return string
* The entity type identifier.
*/
abstract public function getEntityTypeId();
/**
* Returns the language manager.
*
* @return \Drupal\Core\Language\LanguageManagerInterface
* The language manager.
*/
abstract protected function getLanguageManager();
/**
* Returns the top object of a view.
*
* @return \Drupal\views\ViewExecutable
* The view object.
*/
abstract protected function getView();
}

View File

@ -0,0 +1,122 @@
<?php
namespace Drupal\views\Entity\Render;
use Drupal\Core\Entity\EntityInterface;
use Drupal\views\Plugin\views\query\QueryPluginBase;
use Drupal\views\ResultRow;
/**
* Defines a base class for entity translation renderers.
*/
abstract class EntityTranslationRendererBase extends RendererBase {
/**
* Returns the language code associated with the given row.
*
* @param \Drupal\views\ResultRow $row
* The result row.
*
* @return string
* A language code.
*/
abstract public function getLangcode(ResultRow $row);
/**
* Returns the language code associated with the given row.
*
* @param \Drupal\views\ResultRow $row
* The result row.
* @param string $relationship
* The relationship to be used.
*
* @return string
* A language code.
*/
public function getLangcodeByRelationship(ResultRow $row, string $relationship): string {
// This method needs to be overridden if the relationship is needed in the
// implementation of getLangcode().
return $this->getLangcode($row);
}
/**
* {@inheritdoc}
*/
public function query(QueryPluginBase $query, $relationship = NULL) {
}
/**
* {@inheritdoc}
*/
public function preRender(array $result) {
$this->preRenderByRelationship($result, 'none');
}
/**
* Runs before each entity is rendered if a relationship is needed.
*
* @param \Drupal\views\ResultRow[] $result
* The full array of results from the query.
* @param string $relationship
* The relationship to be used.
*/
public function preRenderByRelationship(array $result, string $relationship): void {
$view_builder = \Drupal::entityTypeManager()->getViewBuilder($this->entityType->id());
foreach ($result as $row) {
if ($entity = $this->getEntity($row, $relationship)) {
$entity->view = $this->view;
$this->build[$entity->id()] = $view_builder->view($entity, $this->view->rowPlugin->options['view_mode'], $this->getLangcodeByRelationship($row, $relationship));
}
}
}
/**
* {@inheritdoc}
*/
public function render(ResultRow $row) {
return $this->renderByRelationship($row, 'none');
}
/**
* Renders entity data.
*
* @param \Drupal\views\ResultRow $row
* A single row of the query result.
* @param string $relationship
* The relationship to be used.
*
* @return array
* A renderable array for the entity data contained in the result row.
*/
public function renderByRelationship(ResultRow $row, string $relationship): array {
if ($entity = $this->getEntity($row, $relationship)) {
$entity_id = $entity->id();
return $this->build[$entity_id];
}
return [];
}
/**
* Gets the entity associated with a row.
*
* @param \Drupal\views\ResultRow $row
* The result row.
* @param string $relationship
* (optional) The relationship.
*
* @return \Drupal\Core\Entity\EntityInterface|null
* The entity might be optional, because the relationship entity might not
* always exist.
*/
protected function getEntity(ResultRow $row, string $relationship = 'none'): ?EntityInterface {
if ($relationship === 'none') {
return $row->_entity;
}
elseif (isset($row->_relationship_entities[$relationship])) {
return $row->_relationship_entities[$relationship];
}
return NULL;
}
}

View File

@ -0,0 +1,113 @@
<?php
namespace Drupal\views\Entity\Render;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\views\Plugin\views\query\QueryPluginBase;
use Drupal\views\ResultRow;
use Drupal\views\ViewExecutable;
/**
* Defines a base class for entity renderers.
*/
abstract class RendererBase implements CacheableDependencyInterface {
/**
* The view executable wrapping the view storage entity.
*
* @var \Drupal\views\ViewExecutable
*/
public $view;
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* The type of the entity being rendered.
*
* @var \Drupal\Core\Entity\EntityTypeInterface
*/
protected $entityType;
/**
* Contains an array of render arrays, one for each rendered entity.
*
* @var array
*/
protected $build;
/**
* Constructs a renderer object.
*
* @param \Drupal\views\ViewExecutable $view
* The entity row being rendered.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
*/
public function __construct(ViewExecutable $view, LanguageManagerInterface $language_manager, EntityTypeInterface $entity_type) {
$this->view = $view;
$this->languageManager = $language_manager;
$this->entityType = $entity_type;
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
return Cache::PERMANENT;
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
return [];
}
/**
* {@inheritdoc}
*/
public function getCacheTags() {
return [];
}
/**
* Alters the query if needed.
*
* @param \Drupal\views\Plugin\views\query\QueryPluginBase $query
* The query to alter.
* @param string $relationship
* (optional) The relationship, used by a field.
*/
abstract public function query(QueryPluginBase $query, $relationship = NULL);
/**
* Runs before each entity is rendered.
*
* @param \Drupal\views\ResultRow[] $result
* The full array of results from the query.
*/
public function preRender(array $result) {
}
/**
* Renders entity data.
*
* @param \Drupal\views\ResultRow $row
* A single row of the query result.
*
* @return array
* A renderable array for the entity data contained in the result row.
*/
abstract public function render(ResultRow $row);
}

View File

@ -0,0 +1,119 @@
<?php
namespace Drupal\views\Entity\Render;
use Drupal\Core\Language\LanguageInterface;
use Drupal\views\Plugin\views\query\QueryPluginBase;
use Drupal\views\ResultRow;
/**
* Renders entity translations in their row language.
*/
class TranslationLanguageRenderer extends EntityTranslationRendererBase {
/**
* Stores the field alias of the langcode column.
*
* @var string
*/
protected $langcodeAlias;
/**
* {@inheritdoc}
*/
public function query(QueryPluginBase $query, $relationship = NULL) {
// In order to render in the translation language of the entity, we need
// to add the language code of the entity to the query. Skip if the site
// is not multilingual or the entity is not translatable.
if (!$this->languageManager->isMultilingual() || !$this->entityType->hasKey('langcode')) {
return;
}
$langcode_table = $this->getLangcodeTable($query, $relationship);
if ($langcode_table) {
/** @var \Drupal\views\Plugin\views\query\Sql $query */
$table_alias = $query->ensureTable($langcode_table, $relationship);
$langcode_key = $this->entityType->getKey('langcode');
$this->langcodeAlias = $query->addField($table_alias, $langcode_key);
}
}
/**
* Returns the name of the table holding the "langcode" field.
*
* @param \Drupal\views\Plugin\views\query\QueryPluginBase $query
* The query being executed.
* @param string $relationship
* The relationship used by the entity type.
*
* @return string
* A table name.
*/
protected function getLangcodeTable(QueryPluginBase $query, $relationship) {
/** @var \Drupal\Core\Entity\Sql\SqlContentEntityStorage $storage */
$storage = \Drupal::entityTypeManager()->getStorage($this->entityType->id());
$langcode_key = $this->entityType->getKey('langcode');
$langcode_table = $storage->getTableMapping()->getFieldTableName($langcode_key);
// If the entity type is revisionable, we need to take into account views of
// entity revisions. Usually the view will use the entity data table as the
// query base table, however, in case of an entity revision view, we need to
// use the revision table or the revision data table, depending on which one
// is being used as query base table.
if ($this->entityType->isRevisionable()) {
$query_base_table = $query->relationships[$relationship]['base'] ??
$this->view->storage->get('base_table');
$revision_table = $storage->getRevisionTable();
$revision_data_table = $storage->getRevisionDataTable();
if ($query_base_table === $revision_table) {
$langcode_table = $revision_table;
}
elseif ($query_base_table === $revision_data_table) {
$langcode_table = $revision_data_table;
}
}
return $langcode_table;
}
/**
* {@inheritdoc}
*/
public function preRenderByRelationship(array $result, string $relationship): void {
$view_builder = \Drupal::entityTypeManager()->getViewBuilder($this->entityType->id());
/** @var \Drupal\views\ResultRow $row */
foreach ($result as $row) {
if ($entity = $this->getEntity($row, $relationship)) {
$entity->view = $this->view;
$langcode = $this->getLangcodeByRelationship($row, $relationship);
$this->build[$entity->id()][$langcode] = $view_builder->view($entity, $this->view->rowPlugin->options['view_mode'], $langcode);
}
}
}
/**
* {@inheritdoc}
*/
public function renderByRelationship(ResultRow $row, string $relationship): array {
if ($entity = $this->getEntity($row, $relationship)) {
$entity_id = $entity->id();
return $this->build[$entity_id][$this->getLangcodeByRelationship($row, $relationship)];
}
return [];
}
/**
* {@inheritdoc}
*/
public function getLangcode(ResultRow $row) {
return $row->{$this->langcodeAlias} ?? $this->languageManager->getDefaultLanguage()->getId();
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
return ['languages:' . LanguageInterface::TYPE_CONTENT];
}
}

View File

@ -0,0 +1,537 @@
<?php
namespace Drupal\views\Entity;
use Drupal\Core\Entity\Attribute\ConfigEntityType;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\views\Plugin\DependentWithRemovalPluginInterface;
use Drupal\views\Views;
use Drupal\views\ViewEntityInterface;
/**
* Defines a View configuration entity class.
*/
#[ConfigEntityType(
id: 'view',
label: new TranslatableMarkup('View', ['context' => 'View entity type']),
label_collection: new TranslatableMarkup('Views', ['context' => 'View entity type']),
label_singular: new TranslatableMarkup('view', ['context' => 'View entity type']),
label_plural: new TranslatableMarkup('views', ['context' => 'View entity type']),
entity_keys: [
'id' => 'id',
'label' => 'label',
'status' => 'status',
],
admin_permission: 'administer views',
label_count: [
'singular' => '@count view',
'plural' => '@count views',
],
config_export: [
'id',
'label',
'module',
'description',
'tag',
'base_table',
'base_field',
'display',
],
)]
class View extends ConfigEntityBase implements ViewEntityInterface {
use StringTranslationTrait;
/**
* The name of the base table this view will use.
*
* @var string
*/
protected $base_table = 'node';
/**
* The unique ID of the view.
*
* @var string
*/
protected $id = NULL;
/**
* The label of the view.
*
* @var string
*/
protected $label;
/**
* The description of the view, which is used only in the interface.
*
* @var string
*/
protected $description = '';
/**
* The "tags" of a view.
*
* The tags are stored as a single string, though it is used as multiple tags
* for example in the views overview.
*
* @var string
*/
protected $tag = '';
/**
* Stores all display handlers of this view.
*
* An array containing Drupal\views\Plugin\views\display\DisplayPluginBase
* objects.
*
* @var array
*/
protected $display = [];
/**
* The name of the base field to use.
*
* @var string
*/
protected $base_field = 'nid';
/**
* Stores a reference to the executable version of this view.
*
* @var \Drupal\views\ViewExecutable
*/
protected $executable;
/**
* The module implementing this view.
*
* @var string
*/
protected $module = 'views';
/**
* {@inheritdoc}
*/
public function getExecutable() {
// Ensure that an executable View is available.
if (!isset($this->executable)) {
$this->executable = Views::executableFactory()->get($this);
}
return $this->executable;
}
/**
* {@inheritdoc}
*/
public function createDuplicate() {
$duplicate = parent::createDuplicate();
unset($duplicate->executable);
return $duplicate;
}
/**
* {@inheritdoc}
*/
public function label() {
return $this->get('label');
}
/**
* {@inheritdoc}
*/
public function addDisplay($plugin_id = 'page', $title = NULL, $id = NULL) {
if (empty($plugin_id)) {
return FALSE;
}
$plugin = Views::pluginManager('display')->getDefinition($plugin_id);
if (empty($plugin)) {
$plugin['title'] = $this->t('Broken');
}
if (empty($id)) {
$id = $this->generateDisplayId($plugin_id);
// Generate a unique human-readable name by inspecting the counter at the
// end of the previous display ID, e.g., 'page_1'.
if ($id !== 'default') {
preg_match("/[0-9]+/", $id, $count);
$count = $count[0];
}
else {
$count = '';
}
if (empty($title)) {
// If there is no title provided, use the plugin title, and if there are
// multiple displays, append the count.
$title = $plugin['title'];
if ($count > 1) {
$title .= ' ' . $count;
}
}
}
$display_options = [
'display_plugin' => $plugin_id,
'id' => $id,
// Cast the display title to a string since it is an object.
// @see \Drupal\Core\StringTranslation\TranslatableMarkup
'display_title' => (string) $title,
'position' => $id === 'default' ? 0 : count($this->display),
'display_options' => [],
];
// Add the display options to the view.
$this->display[$id] = $display_options;
return $id;
}
/**
* Generates a display ID of a certain plugin type.
*
* @param string $plugin_id
* Which plugin should be used for the new display ID.
*
* @return string
* The generated display ID.
*/
protected function generateDisplayId($plugin_id) {
// 'default' is singular and is unique, so just go with 'default'
// for it. For all others, start counting.
if ($plugin_id == 'default') {
return 'default';
}
// Initial ID.
$id = $plugin_id . '_1';
$count = 1;
// Loop through IDs based upon our style plugin name until
// we find one that is unused.
while (!empty($this->display[$id])) {
$id = $plugin_id . '_' . ++$count;
}
return $id;
}
/**
* {@inheritdoc}
*/
public function &getDisplay($display_id) {
return $this->display[$display_id];
}
/**
* {@inheritdoc}
*/
public function duplicateDisplayAsType($old_display_id, $new_display_type) {
$executable = $this->getExecutable();
$display = $executable->newDisplay($new_display_type);
$new_display_id = $display->display['id'];
$displays = $this->get('display');
// Let the display title be generated by the addDisplay method and set the
// right display plugin, but keep the rest from the original display.
$display_duplicate = $displays[$old_display_id];
unset($display_duplicate['display_title']);
unset($display_duplicate['display_plugin']);
unset($display_duplicate['new_id']);
$displays[$new_display_id] = NestedArray::mergeDeep($displays[$new_display_id], $display_duplicate);
$displays[$new_display_id]['id'] = $new_display_id;
// First set the displays.
$this->set('display', $displays);
// Ensure that we just copy display options, which are provided by the new
// display plugin.
$executable->setDisplay($new_display_id);
$executable->display_handler->filterByDefinedOptions($displays[$new_display_id]['display_options']);
// Update the display settings.
$this->set('display', $displays);
return $new_display_id;
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
parent::calculateDependencies();
// Ensure that the view is dependant on the module that implements the view.
$this->addDependency('module', $this->module);
$executable = $this->getExecutable();
$executable->initDisplay();
$executable->initStyle();
foreach ($executable->displayHandlers as $display) {
// Calculate the dependencies each display has.
$this->calculatePluginDependencies($display);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function preSave(EntityStorageInterface $storage) {
parent::preSave($storage);
$displays = $this->get('display');
// Sort the displays.
ksort($displays);
$this->set('display', ['default' => $displays['default']] + $displays);
// Calculating the cacheability metadata is only needed when the view is
// saved through the UI or API. It should not be done when we are syncing
// configuration or installing modules.
if (!$this->isSyncing() && !$this->hasTrustedData()) {
$this->addCacheMetadata();
}
}
/**
* Fills in the cache metadata of this view.
*
* Cache metadata is set per view and per display, and ends up being stored in
* the view's configuration. This allows Views to determine very efficiently:
* - the max-age
* - the cache contexts
* - the cache tags
*
* In other words: this allows us to do the (expensive) work of initializing
* Views plugins and handlers to determine their effect on the cacheability of
* a view at save time rather than at runtime.
*/
protected function addCacheMetadata() {
$executable = $this->getExecutable();
$current_display = $executable->current_display;
$displays = $this->get('display');
foreach (array_keys($displays) as $display_id) {
$display =& $this->getDisplay($display_id);
$executable->setDisplay($display_id);
$cache_metadata = $executable->getDisplay()->calculateCacheMetadata();
$display['cache_metadata']['max-age'] = $cache_metadata->getCacheMaxAge();
$display['cache_metadata']['contexts'] = $cache_metadata->getCacheContexts();
$display['cache_metadata']['tags'] = $cache_metadata->getCacheTags();
// Always include at least the 'languages:' context as there will most
// probably be translatable strings in the view output.
$display['cache_metadata']['contexts'] = Cache::mergeContexts($display['cache_metadata']['contexts'], ['languages:' . LanguageInterface::TYPE_INTERFACE]);
sort($display['cache_metadata']['tags']);
sort($display['cache_metadata']['contexts']);
}
// Restore the previous active display.
$executable->setDisplay($current_display);
}
/**
* {@inheritdoc}
*/
public function postSave(EntityStorageInterface $storage, $update = TRUE) {
parent::postSave($storage, $update);
// @todo Remove if views implements a view_builder controller.
views_invalidate_cache();
$this->invalidateCaches();
// Rebuild the router if this is a new view, or its status changed.
if (!$this->getOriginal() || ($this->status() != $this->getOriginal()->status())) {
\Drupal::service('router.builder')->setRebuildNeeded();
}
}
/**
* {@inheritdoc}
*/
public static function postLoad(EntityStorageInterface $storage, array &$entities) {
parent::postLoad($storage, $entities);
foreach ($entities as $entity) {
$entity->mergeDefaultDisplaysOptions();
}
}
/**
* {@inheritdoc}
*/
public static function preCreate(EntityStorageInterface $storage, array &$values) {
parent::preCreate($storage, $values);
// If there is no information about displays available add at least the
// default display.
$values += [
'display' => [
'default' => [
'display_plugin' => 'default',
'id' => 'default',
'display_title' => 'Default',
'position' => 0,
'display_options' => [],
],
],
];
}
/**
* {@inheritdoc}
*/
public function postCreate(EntityStorageInterface $storage) {
parent::postCreate($storage);
$this->mergeDefaultDisplaysOptions();
}
/**
* {@inheritdoc}
*/
public static function preDelete(EntityStorageInterface $storage, array $entities) {
parent::preDelete($storage, $entities);
// Call the remove() hook on the individual displays.
/** @var \Drupal\views\ViewEntityInterface $entity */
foreach ($entities as $entity) {
$executable = Views::executableFactory()->get($entity);
foreach ($entity->get('display') as $display_id => $display) {
$executable->setDisplay($display_id);
$executable->getDisplay()->remove();
}
}
}
/**
* {@inheritdoc}
*/
public static function postDelete(EntityStorageInterface $storage, array $entities) {
parent::postDelete($storage, $entities);
$tempstore = \Drupal::service('tempstore.shared')->get('views');
foreach ($entities as $entity) {
$tempstore->delete($entity->id());
}
views_invalidate_cache();
}
/**
* {@inheritdoc}
*/
public function mergeDefaultDisplaysOptions() {
$displays = [];
foreach ($this->get('display') as $key => $options) {
$options += [
'display_options' => [],
'display_plugin' => NULL,
'id' => NULL,
'display_title' => '',
'position' => NULL,
];
// Add the defaults for the display.
$displays[$key] = $options;
}
$this->set('display', $displays);
}
/**
* {@inheritdoc}
*/
public function isInstallable() {
$table_definition = \Drupal::service('views.views_data')->get($this->base_table);
// Check whether the base table definition exists and contains a base table
// definition. For example, taxonomy_views_data_alter() defines
// node_field_data even if it doesn't exist as a base table.
return $table_definition && isset($table_definition['table']['base']);
}
/**
* {@inheritdoc}
*/
public function __sleep(): array {
$keys = parent::__sleep();
unset($keys[array_search('executable', $keys)]);
return $keys;
}
/**
* Invalidates cache tags.
*/
public function invalidateCaches() {
// Invalidate cache tags for cached rows.
$tags = $this->getCacheTags();
\Drupal::service('cache_tags.invalidator')->invalidateTags($tags);
}
/**
* {@inheritdoc}
*/
public function onDependencyRemoval(array $dependencies) {
$changed = parent::onDependencyRemoval($dependencies);
// Don't intervene if the views module is removed.
if (isset($dependencies['module']) && in_array('views', $dependencies['module'])) {
return FALSE;
}
// If the base table for the View is provided by a module being removed, we
// delete the View because this is not something that can be fixed manually.
$views_data = Views::viewsData();
$base_table = $this->get('base_table');
$base_table_data = $views_data->get($base_table);
if (!empty($base_table_data['table']['provider']) && in_array($base_table_data['table']['provider'], $dependencies['module'])) {
return FALSE;
}
$current_display = $this->getExecutable()->current_display;
$handler_types = Views::getHandlerTypes();
// Find all the handlers and check whether they want to do something on
// dependency removal.
foreach ($this->display as $display_id => $display_plugin_base) {
$this->getExecutable()->setDisplay($display_id);
$display = $this->getExecutable()->getDisplay();
foreach (array_keys($handler_types) as $handler_type) {
$handlers = $display->getHandlers($handler_type);
foreach ($handlers as $handler_id => $handler) {
if ($handler instanceof DependentWithRemovalPluginInterface) {
if ($handler->onDependencyRemoval($dependencies)) {
// Remove the handler and indicate we made changes.
unset($this->display[$display_id]['display_options'][$handler_types[$handler_type]['plural']][$handler_id]);
$changed = TRUE;
}
}
}
}
}
// Disable the View if we made changes.
// @todo https://www.drupal.org/node/2832558 Give better feedback for
// disabled config.
if ($changed) {
// Force a recalculation of the dependencies if we made changes.
$this->getExecutable()->current_display = NULL;
$this->calculateDependencies();
$this->disable();
}
$this->getExecutable()->setDisplay($current_display);
return $changed;
}
}

View File

@ -0,0 +1,732 @@
<?php
namespace Drupal\views;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Entity\ContentEntityType;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityHandlerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\Sql\SqlEntityStorageInterface;
use Drupal\Core\Entity\Sql\TableMappingInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides generic views integration for entities.
*/
class EntityViewsData implements EntityHandlerInterface, EntityViewsDataInterface {
use StringTranslationTrait;
/**
* Entity type for this views data handler instance.
*
* @var \Drupal\Core\Entity\EntityTypeInterface
*/
protected $entityType;
/**
* The storage used for this entity type.
*
* @var \Drupal\Core\Entity\Sql\SqlEntityStorageInterface
*/
protected $storage;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The translation manager.
*
* @var \Drupal\Core\StringTranslation\TranslationInterface
*/
protected $translationManager;
/**
* The field storage definitions for all base fields of the entity type.
*
* @var \Drupal\Core\Field\FieldStorageDefinitionInterface[]
*
* @deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. No
* replacement is provided.
*
* @see https://www.drupal.org/node/3240278
*/
protected $fieldStorageDefinitions;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The entity field manager.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected $entityFieldManager;
/**
* Constructs an EntityViewsData object.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type to provide views integration for.
* @param \Drupal\Core\Entity\Sql\SqlEntityStorageInterface $storage_controller
* The storage handler used for this entity type.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\StringTranslation\TranslationInterface $translation_manager
* The translation manager.
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
* The entity field manager.
*/
public function __construct(EntityTypeInterface $entity_type, SqlEntityStorageInterface $storage_controller, EntityTypeManagerInterface $entity_type_manager, ModuleHandlerInterface $module_handler, TranslationInterface $translation_manager, EntityFieldManagerInterface $entity_field_manager) {
$this->entityType = $entity_type;
$this->entityTypeManager = $entity_type_manager;
$this->storage = $storage_controller;
$this->moduleHandler = $module_handler;
$this->setStringTranslation($translation_manager);
$this->entityFieldManager = $entity_field_manager;
}
/**
* {@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('entity_type.manager'),
$container->get('module_handler'),
$container->get('string_translation'),
$container->get('entity_field.manager')
);
}
/**
* Gets the field storage definitions.
*
* @return \Drupal\Core\Field\FieldStorageDefinitionInterface[]
* The array of field storage definitions, keyed by field name.
*
* @deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. No
* replacement is provided.
*
* @see https://www.drupal.org/node/3240278
*/
protected function getFieldStorageDefinitions() {
@trigger_error(__METHOD__ . '() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. No replacement is provided. See https://www.drupal.org/node/3240278', E_USER_DEPRECATED);
if (!isset($this->fieldStorageDefinitions)) {
$this->fieldStorageDefinitions = $this->entityFieldManager->getFieldStorageDefinitions($this->entityType->id());
}
return $this->fieldStorageDefinitions;
}
/**
* {@inheritdoc}
*/
public function getViewsData() {
$data = [];
$base_table = $this->entityType->getBaseTable() ?: $this->entityType->id();
$views_revision_base_table = NULL;
$revisionable = $this->entityType->isRevisionable();
$entity_id_key = $this->entityType->getKey('id');
$entity_keys = $this->entityType->getKeys();
$revision_table = '';
if ($revisionable) {
$revision_table = $this->entityType->getRevisionTable() ?: $this->entityType->id() . '_revision';
}
$translatable = $this->entityType->isTranslatable();
$data_table = '';
if ($translatable) {
$data_table = $this->entityType->getDataTable() ?: $this->entityType->id() . '_field_data';
}
// Some entity types do not have a revision data table defined, but still
// have a revision table name set in
// \Drupal\Core\Entity\Sql\SqlContentEntityStorage::initTableLayout() so we
// apply the same kind of logic.
$revision_data_table = '';
if ($revisionable && $translatable) {
$revision_data_table = $this->entityType->getRevisionDataTable() ?: $this->entityType->id() . '_field_revision';
}
$entity_revision_key = $this->entityType->getKey('revision');
$revision_field = $entity_revision_key;
// Setup base information of the views data.
$data[$base_table]['table']['group'] = $this->entityType->getLabel();
$data[$base_table]['table']['provider'] = $this->entityType->getProvider();
$views_base_table = $base_table;
if ($data_table) {
$views_base_table = $data_table;
}
$data[$views_base_table]['table']['base'] = [
'field' => $entity_id_key,
'title' => $this->entityType->getLabel(),
'cache_contexts' => $this->entityType->getListCacheContexts(),
'access query tag' => $this->entityType->id() . '_access',
];
$data[$base_table]['table']['entity revision'] = FALSE;
if ($label_key = $this->entityType->getKey('label')) {
if ($data_table) {
$data[$views_base_table]['table']['base']['defaults'] = [
'field' => $label_key,
'table' => $data_table,
];
}
else {
$data[$views_base_table]['table']['base']['defaults'] = [
'field' => $label_key,
];
}
}
// Entity types must implement a list_builder in order to use Views'
// entity operations field.
if ($this->entityType->hasListBuilderClass()) {
$data[$base_table]['operations'] = [
'field' => [
'title' => $this->t('Operations links'),
'help' => $this->t('Provides links to perform entity operations.'),
'id' => 'entity_operations',
],
];
if ($revision_table) {
$data[$revision_table]['operations'] = [
'field' => [
'title' => $this->t('Operations links'),
'help' => $this->t('Provides links to perform entity operations.'),
'id' => 'entity_operations',
],
];
}
}
if ($this->entityType->hasViewBuilderClass()) {
$data[$base_table]['rendered_entity'] = [
'field' => [
'title' => $this->t('Rendered entity'),
'help' => $this->t('Renders an entity in a view mode.'),
'id' => 'rendered_entity',
],
];
}
// Setup relations to the revisions/property data.
if ($data_table) {
$data[$base_table]['table']['join'][$data_table] = [
'left_field' => $entity_id_key,
'field' => $entity_id_key,
'type' => 'INNER',
];
$data[$data_table]['table']['group'] = $this->entityType->getLabel();
$data[$data_table]['table']['provider'] = $this->entityType->getProvider();
$data[$data_table]['table']['entity revision'] = FALSE;
}
if ($revision_table) {
$data[$revision_table]['table']['group'] = $this->t('@entity_type revision', ['@entity_type' => $this->entityType->getLabel()]);
$data[$revision_table]['table']['provider'] = $this->entityType->getProvider();
$data[$revision_table]['table']['entity revision'] = TRUE;
$views_revision_base_table = $revision_table;
if ($revision_data_table) {
$views_revision_base_table = $revision_data_table;
}
$data[$views_revision_base_table]['table']['entity revision'] = TRUE;
$data[$views_revision_base_table]['table']['base'] = [
'field' => $revision_field,
'title' => $this->t('@entity_type revisions', ['@entity_type' => $this->entityType->getLabel()]),
];
// Join the revision table to the base table.
$data[$views_revision_base_table]['table']['join'][$views_base_table] = [
'left_field' => $revision_field,
'field' => $revision_field,
'type' => 'INNER',
];
if ($revision_data_table) {
$data[$revision_data_table]['table']['group'] = $this->t('@entity_type revision', ['@entity_type' => $this->entityType->getLabel()]);
$data[$revision_data_table]['table']['entity revision'] = TRUE;
$data[$revision_table]['table']['join'][$revision_data_table] = [
'left_field' => $revision_field,
'field' => $revision_field,
'type' => 'INNER',
];
}
// Add a filter for showing only the latest revisions of an entity.
$data[$revision_table]['latest_revision'] = [
'title' => $this->t('Is Latest Revision'),
'help' => $this->t('Restrict the view to only revisions that are the latest revision of their entity.'),
'filter' => ['id' => 'latest_revision'],
];
if ($this->entityType->isTranslatable()) {
$data[$revision_table]['latest_translation_affected_revision'] = [
'title' => $this->t('Is Latest Translation Affected Revision'),
'help' => $this->t('Restrict the view to only revisions that are the latest translation affected revision of their entity.'),
'filter' => ['id' => 'latest_translation_affected_revision'],
];
}
// Add a relationship from the revision table back to the main table.
$entity_type_label = $this->entityType->getLabel();
$data[$views_revision_base_table][$entity_id_key]['relationship'] = [
'id' => 'standard',
'base' => $views_base_table,
'base field' => $entity_id_key,
'title' => $entity_type_label,
'help' => $this->t('Get the actual @label from a @label revision', ['@label' => $entity_type_label]),
];
$data[$views_revision_base_table][$entity_revision_key]['relationship'] = [
'id' => 'standard',
'base' => $views_base_table,
'base field' => $entity_revision_key,
'title' => $this->t('@label revision', ['@label' => $entity_type_label]),
'help' => $this->t('Get the actual @label from a @label revision', ['@label' => $entity_type_label]),
];
if ($translatable) {
$extra = [
'field' => $entity_keys['langcode'],
'left_field' => $entity_keys['langcode'],
];
$data[$views_revision_base_table][$entity_id_key]['relationship']['extra'][] = $extra;
$data[$views_revision_base_table][$entity_revision_key]['relationship']['extra'][] = $extra;
$data[$revision_table]['table']['join'][$views_base_table]['left_field'] = $entity_revision_key;
$data[$revision_table]['table']['join'][$views_base_table]['field'] = $entity_revision_key;
}
}
$this->addEntityLinks($data[$base_table]);
if ($views_revision_base_table) {
$this->addEntityLinks($data[$views_revision_base_table]);
}
// Load all typed data definitions of all fields. This should cover each of
// the entity base, revision, data tables.
$field_definitions = $this->entityFieldManager->getBaseFieldDefinitions($this->entityType->id());
$field_storage_definitions = array_map(function (FieldDefinitionInterface $definition) {
return $definition->getFieldStorageDefinition();
}, $field_definitions);
/** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */
$table_mapping = $this->storage->getTableMapping($field_storage_definitions);
// Fetch all fields that can appear in both the base table and the data
// table.
$duplicate_fields = array_intersect_key($entity_keys, array_flip(['id', 'revision', 'bundle']));
// Iterate over each table we have so far and collect field data for each.
// Based on whether the field is in the field_definitions provided by the
// entity field manager.
// @todo We should better just rely on information coming from the entity
// storage.
// @todo https://www.drupal.org/node/2337511
foreach ($table_mapping->getTableNames() as $table) {
foreach ($table_mapping->getFieldNames($table) as $field_name) {
// To avoid confusing duplication in the user interface, for fields
// that are on both base and data tables, only add them on the data
// table (same for revision vs. revision data).
if ($data_table && ($table === $base_table || $table === $revision_table) && in_array($field_name, $duplicate_fields)) {
continue;
}
$this->mapFieldDefinition($table, $field_name, $field_definitions[$field_name], $table_mapping, $data[$table]);
}
}
foreach ($field_storage_definitions as $field_storage_definition) {
if ($table_mapping->requiresDedicatedTableStorage($field_storage_definition)) {
$table = $table_mapping->getDedicatedDataTableName($field_storage_definition);
$data[$table]['table']['group'] = $this->entityType->getLabel();
$data[$table]['table']['provider'] = $this->entityType->getProvider();
$data[$table]['table']['join'][$views_base_table] = [
'left_field' => $entity_id_key,
'field' => 'entity_id',
'extra' => [
['field' => 'deleted', 'value' => 0, 'numeric' => TRUE],
],
];
if ($revisionable) {
$revision_table = $table_mapping->getDedicatedRevisionTableName($field_storage_definition);
$data[$revision_table]['table']['group'] = $this->t('@entity_type revision', ['@entity_type' => $this->entityType->getLabel()]);
$data[$revision_table]['table']['provider'] = $this->entityType->getProvider();
$data[$revision_table]['table']['join'][$views_revision_base_table] = [
'left_field' => $revision_field,
'field' => 'entity_id',
'extra' => [
['field' => 'deleted', 'value' => 0, 'numeric' => TRUE],
],
];
}
}
}
if (($uid_key = $entity_keys['uid'] ?? '')) {
$data[$data_table][$uid_key]['filter']['id'] = 'user_name';
}
if ($revision_table && ($revision_uid_key = $this->entityType->getRevisionMetadataKeys()['revision_user'] ?? '')) {
$data[$revision_table][$revision_uid_key]['filter']['id'] = 'user_name';
}
// Add the entity type key to each table generated.
$entity_type_id = $this->entityType->id();
array_walk($data, function (&$table_data) use ($entity_type_id) {
$table_data['table']['entity type'] = $entity_type_id;
});
return $data;
}
/**
* Sets the entity links in case corresponding link templates exist.
*
* @param array $data
* The views data of the base table.
*/
protected function addEntityLinks(array &$data) {
$entity_type_id = $this->entityType->id();
$t_arguments = ['@entity_type_label' => $this->entityType->getLabel()];
if ($this->entityType->hasLinkTemplate('canonical')) {
$data['view_' . $entity_type_id] = [
'field' => [
'title' => $this->t('Link to @entity_type_label', $t_arguments),
'help' => $this->t('Provide a view link to the @entity_type_label.', $t_arguments),
'id' => 'entity_link',
],
];
}
if ($this->entityType->hasLinkTemplate('edit-form')) {
$data['edit_' . $entity_type_id] = [
'field' => [
'title' => $this->t('Link to edit @entity_type_label', $t_arguments),
'help' => $this->t('Provide an edit link to the @entity_type_label.', $t_arguments),
'id' => 'entity_link_edit',
],
];
}
if ($this->entityType->hasLinkTemplate('delete-form')) {
$data['delete_' . $entity_type_id] = [
'field' => [
'title' => $this->t('Link to delete @entity_type_label', $t_arguments),
'help' => $this->t('Provide a delete link to the @entity_type_label.', $t_arguments),
'id' => 'entity_link_delete',
],
];
}
}
/**
* Puts the views data for a single field onto the views data.
*
* @param string $table
* The table of the field to handle.
* @param string $field_name
* The name of the field to handle.
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* The field definition.
* @param \Drupal\Core\Entity\Sql\TableMappingInterface $table_mapping
* The table mapping information.
* @param array $table_data
* A reference to a specific entity table (for example data_table) inside
* the views data.
*/
protected function mapFieldDefinition($table, $field_name, FieldDefinitionInterface $field_definition, TableMappingInterface $table_mapping, &$table_data) {
// Create a dummy instance to retrieve property definitions.
$field_column_mapping = $table_mapping->getColumnNames($field_name);
$field_schema = $field_definition->getFieldStorageDefinition()->getSchema();
$field_definition_type = $field_definition->getType();
// Add all properties to views table data. We need an entry for each
// column of each field, with the first one given special treatment.
// @todo Introduce concept of the "main" column for a field, rather than
// assuming the first one is the main column. See also what the
// mapSingleFieldViewsData() method does with $first.
$first = TRUE;
foreach ($field_column_mapping as $field_column_name => $schema_field_name) {
// The fields might be defined before the actual table.
$table_data = $table_data ?: [];
$table_data += [$schema_field_name => []];
$table_data[$schema_field_name] = NestedArray::mergeDeep($table_data[$schema_field_name], $this->mapSingleFieldViewsData($table, $field_name, $field_definition_type, $field_column_name, $field_schema['columns'][$field_column_name]['type'], $first, $field_definition));
$table_data[$schema_field_name]['entity field'] = $field_name;
$first = FALSE;
}
}
/**
* Provides the views data for a given data type and schema field.
*
* @param string $table
* The table of the field to handle.
* @param string $field_name
* The machine name of the field being processed.
* @param string $field_type
* The type of field being handled.
* @param string $column_name
* For fields containing multiple columns, the column name being processed.
* @param string $column_type
* Within the field, the column type being handled.
* @param bool $first
* TRUE if this is the first column within the field.
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* The field definition.
*
* @return array
* The modified views data field definition.
*/
protected function mapSingleFieldViewsData($table, $field_name, $field_type, $column_name, $column_type, $first, FieldDefinitionInterface $field_definition) {
$views_field = [];
// Provide a nicer, less verbose label for the first column within a field.
// @todo Introduce concept of the "main" column for a field, rather than
// assuming the first one is the main column.
if ($first) {
$views_field['title'] = $field_definition->getLabel();
}
else {
$views_field['title'] = $field_definition->getLabel() . " ($column_name)";
}
if ($description = $field_definition->getDescription()) {
$views_field['help'] = $description;
}
// Set up the field, sort, argument, and filters, based on
// the column and/or field data type.
// @todo Allow field types to customize this.
// @see https://www.drupal.org/node/2337515
switch ($field_type) {
// Special case a few field types.
case 'timestamp':
case 'created':
case 'changed':
$views_field['field']['id'] = 'field';
$views_field['argument']['id'] = 'date';
$views_field['filter']['id'] = 'date';
$views_field['sort']['id'] = 'date';
break;
case 'language':
$views_field['field']['id'] = 'field_language';
$views_field['argument']['id'] = 'language';
$views_field['filter']['id'] = 'language';
$views_field['sort']['id'] = 'standard';
break;
case 'boolean':
$views_field['field']['id'] = 'field';
$views_field['argument']['id'] = 'numeric';
$views_field['filter']['id'] = 'boolean';
$views_field['sort']['id'] = 'standard';
break;
case 'uri':
// Let's render URIs as URIs by default, not links.
$views_field['field']['id'] = 'field';
$views_field['field']['default_formatter'] = 'string';
$views_field['argument']['id'] = 'string';
$views_field['filter']['id'] = 'string';
$views_field['sort']['id'] = 'standard';
break;
case 'text':
case 'text_with_summary':
// Treat these three long text fields the same.
$field_type = 'text_long';
// Intentional fall-through here to the default processing!
default:
// For most fields, the field type is generic enough to just use
// the column type to determine the filters etc.
switch ($column_type) {
case 'int':
case 'integer':
case 'smallint':
case 'tinyint':
case 'mediumint':
case 'float':
case 'double':
case 'decimal':
$views_field['field']['id'] = 'field';
$views_field['argument']['id'] = 'numeric';
$views_field['filter']['id'] = 'numeric';
$views_field['sort']['id'] = 'standard';
break;
case 'char':
case 'string':
case 'varchar':
case 'varchar_ascii':
case 'tinytext':
case 'text':
case 'mediumtext':
case 'longtext':
$views_field['field']['id'] = 'field';
$views_field['argument']['id'] = 'string';
$views_field['filter']['id'] = 'string';
$views_field['sort']['id'] = 'standard';
break;
default:
$views_field['field']['id'] = 'field';
$views_field['argument']['id'] = 'standard';
$views_field['filter']['id'] = 'standard';
$views_field['sort']['id'] = 'standard';
}
}
if (!$field_definition->isRequired()) {
// Provides "Is empty (NULL)" and "Is not empty (NOT NULL)" operators.
$views_field['filter']['allow empty'] = TRUE;
}
// Do post-processing for a few field types.
$process_method = 'processViewsDataFor' . Container::camelize($field_type);
if (method_exists($this, $process_method)) {
$this->{$process_method}($table, $field_definition, $views_field, $column_name);
}
return $views_field;
}
/**
* Processes the views data for a language field.
*
* @param string $table
* The table the language field is added to.
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* The field definition.
* @param array $views_field
* The views field data.
* @param string $field_column_name
* The field column being processed.
*/
protected function processViewsDataForLanguage($table, FieldDefinitionInterface $field_definition, array &$views_field, $field_column_name) {
// Apply special titles for the langcode field.
if ($field_definition->getName() == $this->entityType->getKey('langcode')) {
if ($table == $this->entityType->getDataTable() || $table == $this->entityType->getRevisionDataTable()) {
$views_field['title'] = $this->t('Translation language');
}
if ($table == $this->entityType->getBaseTable() || $table == $this->entityType->getRevisionTable()) {
$views_field['title'] = $this->t('Original language');
}
}
}
/**
* Processes the views data for an entity reference field.
*
* @param string $table
* The table the language field is added to.
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* The field definition.
* @param array $views_field
* The views field data.
* @param string $field_column_name
* The field column being processed.
*/
protected function processViewsDataForEntityReference($table, FieldDefinitionInterface $field_definition, array &$views_field, $field_column_name) {
// @todo Should the actual field handler respect that this just renders a
// number?
// @todo Create an optional entity field handler, that can render the
// entity.
// @see https://www.drupal.org/node/2322949
if ($entity_type_id = $field_definition->getItemDefinition()->getSetting('target_type')) {
$entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
if ($entity_type instanceof ContentEntityType) {
$views_field['relationship'] = [
'base' => $this->getViewsTableForEntityType($entity_type),
'base field' => $entity_type->getKey('id'),
'label' => $entity_type->getLabel(),
'title' => $entity_type->getLabel(),
'id' => 'standard',
];
$views_field['field']['id'] = 'field';
// Provide an argument plugin that has a meaningful titleQuery()
// implementation getting the entity label.
$views_field['argument']['id'] = 'entity_target_id';
$views_field['argument']['target_entity_type_id'] = $entity_type_id;
$views_field['filter']['id'] = 'numeric';
$views_field['sort']['id'] = 'standard';
}
else {
$views_field['field']['id'] = 'field';
$views_field['argument']['id'] = 'string';
$views_field['filter']['id'] = 'string';
$views_field['sort']['id'] = 'standard';
}
}
if ($field_definition->getName() == $this->entityType->getKey('bundle')) {
$views_field['filter']['id'] = 'bundle';
}
}
/**
* Processes the views data for a text field with formatting.
*
* @param string $table
* The table the field is added to.
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* The field definition.
* @param array $views_field
* The views field data.
* @param string $field_column_name
* The field column being processed.
*/
protected function processViewsDataForTextLong($table, FieldDefinitionInterface $field_definition, array &$views_field, $field_column_name) {
// Connect the text field to its formatter.
if ($field_column_name == 'value') {
$views_field['field']['format'] = $field_definition->getName() . '__format';
$views_field['field']['id'] = 'field';
}
}
/**
* Processes the views data for a UUID field.
*
* @param string $table
* The table the field is added to.
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* The field definition.
* @param array $views_field
* The views field data.
* @param string $field_column_name
* The field column being processed.
*/
protected function processViewsDataForUuid($table, FieldDefinitionInterface $field_definition, array &$views_field, $field_column_name) {
// It does not make sense for UUID fields to be click sortable.
$views_field['field']['click sortable'] = FALSE;
}
/**
* {@inheritdoc}
*/
public function getViewsTableForEntityType(EntityTypeInterface $entity_type) {
return $entity_type->getDataTable() ?: $entity_type->getBaseTable();
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace Drupal\views;
use Drupal\Core\Entity\EntityTypeInterface;
/**
* Provides an interface to integrate an entity type with views.
*/
interface EntityViewsDataInterface {
/**
* Returns views data for the entity type.
*
* @return array
* Views data in the format of hook_views_data().
*/
public function getViewsData();
/**
* Gets the table of an entity type to be used as base table in views.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
*
* @return string
* The name of the base table in views.
*/
public function getViewsTableForEntityType(EntityTypeInterface $entity_type);
}

View File

@ -0,0 +1,178 @@
<?php
namespace Drupal\views\EventSubscriber;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\State\StateInterface;
use Drupal\Core\Routing\RouteSubscriberBase;
use Drupal\Core\Routing\RoutingEvents;
use Drupal\views\Plugin\views\display\DisplayRouterInterface;
use Drupal\views\ViewExecutable;
use Drupal\views\Views;
use Symfony\Component\Routing\RouteCollection;
/**
* Builds up the routes of all views.
*
* The general idea is to execute first all alter hooks to determine which
* routes are overridden by views. This information is used to determine which
* views have to be added by views in the dynamic event.
*
* @see \Drupal\views\Plugin\views\display\PathPluginBase
*/
class RouteSubscriber extends RouteSubscriberBase {
/**
* Stores a list of view,display IDs which haven't be used in the alter event.
*
* @var array
*/
protected $viewsDisplayPairs;
/**
* The view storage.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $viewStorage;
/**
* The state key value store.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* Stores an array of route names keyed by view_id.display_id.
*
* @var array
*/
protected $viewRouteNames = [];
/**
* Constructs a \Drupal\views\EventSubscriber\RouteSubscriber instance.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
* @param \Drupal\Core\State\StateInterface $state
* The state key value store.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, StateInterface $state) {
$this->viewStorage = $entity_type_manager->getStorage('view');
$this->state = $state;
}
/**
* Resets the internal state of the route subscriber.
*/
public function reset() {
$this->viewsDisplayPairs = NULL;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
$events = parent::getSubscribedEvents();
$events[RoutingEvents::FINISHED] = ['routeRebuildFinished'];
// Ensure to run after the entity resolver subscriber
// @see \Drupal\Core\EventSubscriber\EntityRouteAlterSubscriber
$events[RoutingEvents::ALTER] = ['onAlterRoutes', -175];
return $events;
}
/**
* Gets all the views and display IDs using a route.
*/
protected function getViewsDisplayIDsWithRoute() {
if (!isset($this->viewsDisplayPairs)) {
$this->viewsDisplayPairs = [];
// @todo Convert this method to some service.
$views = $this->getApplicableViews();
foreach ($views as $data) {
[$view_id, $display_id] = $data;
$this->viewsDisplayPairs[] = $view_id . '.' . $display_id;
}
$this->viewsDisplayPairs = array_combine($this->viewsDisplayPairs, $this->viewsDisplayPairs);
}
return $this->viewsDisplayPairs;
}
/**
* Returns a set of route objects.
*
* @return \Symfony\Component\Routing\RouteCollection
* A route collection.
*/
public function routes() {
$collection = new RouteCollection();
foreach ($this->getViewsDisplayIDsWithRoute() as $pair) {
[$view_id, $display_id] = explode('.', $pair);
$view = $this->viewStorage->load($view_id);
// @todo This should have an executable factory injected.
if (($view = $view->getExecutable()) && $view instanceof ViewExecutable) {
if ($view->setDisplay($display_id) && $display = $view->displayHandlers->get($display_id)) {
if ($display instanceof DisplayRouterInterface) {
$this->viewRouteNames += (array) $display->collectRoutes($collection);
}
}
$view->destroy();
}
}
$this->state->set('views.view_route_names', $this->viewRouteNames);
return $collection;
}
/**
* {@inheritdoc}
*/
protected function alterRoutes(RouteCollection $collection) {
foreach ($this->getViewsDisplayIDsWithRoute() as $pair) {
[$view_id, $display_id] = explode('.', $pair);
$view = $this->viewStorage->load($view_id);
// @todo This should have an executable factory injected.
if (($view = $view->getExecutable()) && $view instanceof ViewExecutable) {
if ($view->setDisplay($display_id) && $display = $view->displayHandlers->get($display_id)) {
if ($display instanceof DisplayRouterInterface) {
// If the display returns TRUE a route item was found, so it does
// not have to be added.
$view_route_names = $display->alterRoutes($collection);
$this->viewRouteNames = $view_route_names + $this->viewRouteNames;
foreach ($view_route_names as $id_display => $route_name) {
$view_route_name = $this->viewsDisplayPairs[$id_display];
unset($this->viewsDisplayPairs[$id_display]);
$collection->remove("views.$view_route_name");
}
}
}
$view->destroy();
}
}
}
/**
* Stores the new route names after they have been rebuilt.
*
* Callback for the RoutingEvents::FINISHED event.
*
* @see \Drupal\views\EventSubscriber::getSubscribedEvents()
*/
public function routeRebuildFinished() {
$this->reset();
$this->state->set('views.view_route_names', $this->viewRouteNames);
}
/**
* Returns all views/display combinations with routes.
*
* @see \Drupal\views\Views::getApplicableViews()
*/
protected function getApplicableViews() {
return Views::getApplicableViews('uses_route');
}
}

View File

@ -0,0 +1,427 @@
<?php
namespace Drupal\views\EventSubscriber;
use Drupal\Core\Entity\EntityTypeEventSubscriberTrait;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeListenerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
use Drupal\views\ViewEntityInterface;
use Drupal\views\Views;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Reacts to changes on entity types to update all views entities.
*/
class ViewsEntitySchemaSubscriber implements EntityTypeListenerInterface, EventSubscriberInterface {
use EntityTypeEventSubscriberTrait;
/**
* Indicates that a base table got renamed.
*/
const BASE_TABLE_RENAME = 0;
/**
* Indicates that a data table got renamed.
*/
const DATA_TABLE_RENAME = 1;
/**
* Indicates that a data table got added.
*/
const DATA_TABLE_ADDITION = 2;
/**
* Indicates that a data table got removed.
*/
const DATA_TABLE_REMOVAL = 3;
/**
* Indicates that a revision table got renamed.
*/
const REVISION_TABLE_RENAME = 4;
/**
* Indicates that a revision table got added.
*/
const REVISION_TABLE_ADDITION = 5;
/**
* Indicates that a revision table got removed.
*/
const REVISION_TABLE_REMOVAL = 6;
/**
* Indicates that a revision data table got renamed.
*/
const REVISION_DATA_TABLE_RENAME = 7;
/**
* Indicates that a revision data table got added.
*/
const REVISION_DATA_TABLE_ADDITION = 8;
/**
* Indicates that a revision data table got removed.
*/
const REVISION_DATA_TABLE_REMOVAL = 9;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The default logger service.
*
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* Array of views that need to be saved, indexed by view name.
*
* @var \Drupal\views\ViewEntityInterface[]
*/
protected $viewsToSave = [];
/**
* Constructs a ViewsEntitySchemaSubscriber.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Psr\Log\LoggerInterface $logger
* A logger instance.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, LoggerInterface $logger) {
$this->entityTypeManager = $entity_type_manager;
$this->logger = $logger;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
return static::getEntityTypeEvents();
}
/**
* {@inheritdoc}
*/
public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
$changes = [];
// We implement a specific logic for table updates, which is bound to the
// default sql content entity storage.
if (!$this->entityTypeManager->getStorage($entity_type->id()) instanceof SqlContentEntityStorage) {
return;
}
if ($entity_type->getBaseTable() != $original->getBaseTable()) {
$changes[] = static::BASE_TABLE_RENAME;
}
$revision_add = $entity_type->isRevisionable() && !$original->isRevisionable();
$revision_remove = !$entity_type->isRevisionable() && $original->isRevisionable();
$translation_add = $entity_type->isTranslatable() && !$original->isTranslatable();
$translation_remove = !$entity_type->isTranslatable() && $original->isTranslatable();
if ($revision_add) {
$changes[] = static::REVISION_TABLE_ADDITION;
}
elseif ($revision_remove) {
$changes[] = static::REVISION_TABLE_REMOVAL;
}
elseif ($entity_type->isRevisionable() && $entity_type->getRevisionTable() != $original->getRevisionTable()) {
$changes[] = static::REVISION_TABLE_RENAME;
}
if ($translation_add) {
$changes[] = static::DATA_TABLE_ADDITION;
}
elseif ($translation_remove) {
$changes[] = static::DATA_TABLE_REMOVAL;
}
elseif ($entity_type->isTranslatable() && $entity_type->getDataTable() != $original->getDataTable()) {
$changes[] = static::DATA_TABLE_RENAME;
}
if ($entity_type->isRevisionable() && $entity_type->isTranslatable()) {
if ($revision_add || $translation_add) {
$changes[] = static::REVISION_DATA_TABLE_ADDITION;
}
elseif ($entity_type->getRevisionDataTable() != $original->getRevisionDataTable()) {
$changes[] = static::REVISION_DATA_TABLE_RENAME;
}
}
elseif ($original->isRevisionable() && $original->isTranslatable() && ($revision_remove || $translation_remove)) {
$changes[] = static::REVISION_DATA_TABLE_REMOVAL;
}
// Stop here if no changes are needed.
if (empty($changes)) {
return;
}
/** @var \Drupal\views\Entity\View[] $all_views */
$all_views = $this->entityTypeManager->getStorage('view')->loadMultiple(NULL);
foreach ($changes as $change) {
switch ($change) {
case static::BASE_TABLE_RENAME:
$this->baseTableRename($all_views, $entity_type->id(), $original->getBaseTable(), $entity_type->getBaseTable());
break;
case static::DATA_TABLE_RENAME:
$this->dataTableRename($all_views, $entity_type->id(), $original->getDataTable(), $entity_type->getDataTable());
break;
case static::DATA_TABLE_ADDITION:
$this->dataTableAddition($all_views, $entity_type, $entity_type->getDataTable(), $entity_type->getBaseTable());
break;
case static::DATA_TABLE_REMOVAL:
$this->dataTableRemoval($all_views, $entity_type->id(), $original->getDataTable(), $entity_type->getBaseTable());
break;
case static::REVISION_TABLE_RENAME:
$this->baseTableRename($all_views, $entity_type->id(), $original->getRevisionTable(), $entity_type->getRevisionTable());
break;
case static::REVISION_TABLE_ADDITION:
// If we add revision support we don't have to do anything.
break;
case static::REVISION_TABLE_REMOVAL:
$this->revisionRemoval($all_views, $original);
break;
case static::REVISION_DATA_TABLE_RENAME:
$this->dataTableRename($all_views, $entity_type->id(), $original->getRevisionDataTable(), $entity_type->getRevisionDataTable());
break;
case static::REVISION_DATA_TABLE_ADDITION:
$this->dataTableAddition($all_views, $entity_type, $entity_type->getRevisionDataTable(), $entity_type->getRevisionTable());
break;
case static::REVISION_DATA_TABLE_REMOVAL:
$this->dataTableRemoval($all_views, $entity_type->id(), $original->getRevisionDataTable(), $entity_type->getRevisionTable());
break;
}
}
foreach ($this->viewsToSave as $view) {
try {
// All changes done to the views here can be trusted and this might be
// called during updates, when it is not safe to rely on configuration
// containing valid schema. Trust the data and disable schema validation
// and casting.
$view->trustData()->save();
}
catch (\Exception) {
// In case the view could not be saved, log an error message that the
// view needs to be updated manually instead of failing the entire
// entity update process.
$this->logger->critical("The %view_id view could not be updated automatically while processing an entity schema update for the %entity_type_id entity type.", [
'%view_id' => $view->id(),
'%entity_type_id' => $entity_type->id(),
]);
}
}
$this->viewsToSave = [];
}
/**
* {@inheritdoc}
*/
public function onEntityTypeDelete(EntityTypeInterface $entity_type) {
$tables = [
$entity_type->getBaseTable(),
$entity_type->getDataTable(),
$entity_type->getRevisionTable(),
$entity_type->getRevisionDataTable(),
];
$all_views = $this->entityTypeManager->getStorage('view')->loadMultiple(NULL);
/** @var \Drupal\views\Entity\View $view */
foreach ($all_views as $view) {
// First check just the base table.
if (in_array($view->get('base_table'), $tables)) {
$view->disable();
$view->save();
}
}
}
/**
* Applies a callable onto all handlers of all passed in views.
*
* @param \Drupal\views\Entity\View[] $all_views
* All views entities.
* @param callable $process
* A callable which retrieves a handler config array.
*/
protected function processHandlers(array $all_views, callable $process) {
foreach ($all_views as $view) {
foreach (array_keys($view->get('display')) as $display_id) {
$display = &$view->getDisplay($display_id);
foreach (Views::getHandlerTypes() as $handler_type) {
$handler_type = $handler_type['plural'];
if (!isset($display['display_options'][$handler_type])) {
continue;
}
foreach ($display['display_options'][$handler_type] as $id => &$handler_config) {
$process($handler_config, $view);
if ($handler_config === NULL) {
unset($display['display_options'][$handler_type][$id]);
}
}
}
}
}
}
/**
* Updates views if a base table is renamed.
*
* @param \Drupal\views\Entity\View[] $all_views
* All views.
* @param string $entity_type_id
* The entity type ID.
* @param string $old_base_table
* The old base table name.
* @param string $new_base_table
* The new base table name.
*/
protected function baseTableRename($all_views, $entity_type_id, $old_base_table, $new_base_table) {
foreach ($all_views as $view) {
if ($view->get('base_table') == $old_base_table) {
$view->set('base_table', $new_base_table);
$this->viewsToSave[$view->id()] = $view;
}
}
$this->processHandlers($all_views, function (&$handler_config, ViewEntityInterface $view) use ($entity_type_id, $old_base_table, $new_base_table) {
if (isset($handler_config['entity_type']) && $handler_config['entity_type'] == $entity_type_id && $handler_config['table'] == $old_base_table) {
$handler_config['table'] = $new_base_table;
$this->viewsToSave[$view->id()] = $view;
}
});
}
/**
* Updates views if a data table is renamed.
*
* @param \Drupal\views\Entity\View[] $all_views
* All views.
* @param string $entity_type_id
* The entity type ID.
* @param string $old_data_table
* The old data table name.
* @param string $new_data_table
* The new data table name.
*/
protected function dataTableRename($all_views, $entity_type_id, $old_data_table, $new_data_table) {
foreach ($all_views as $view) {
if ($view->get('base_table') == $old_data_table) {
$view->set('base_table', $new_data_table);
$this->viewsToSave[$view->id()] = $view;
}
}
$this->processHandlers($all_views, function (&$handler_config, ViewEntityInterface $view) use ($entity_type_id, $old_data_table, $new_data_table) {
if (isset($handler_config['entity_type']) && $handler_config['entity_type'] == $entity_type_id && $handler_config['table'] == $old_data_table) {
$handler_config['table'] = $new_data_table;
$this->viewsToSave[$view->id()] = $view;
}
});
}
/**
* Updates views if a data table is added.
*
* @param \Drupal\views\Entity\View[] $all_views
* All views.
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
* @param string $new_data_table
* The new data table.
* @param string $base_table
* The base table.
*/
protected function dataTableAddition($all_views, EntityTypeInterface $entity_type, $new_data_table, $base_table) {
/** @var \Drupal\Core\Entity\Sql\SqlContentEntityStorage $storage */
$entity_type_id = $entity_type->id();
$storage = $this->entityTypeManager->getStorage($entity_type_id);
$storage->setEntityType($entity_type);
$table_mapping = $storage->getTableMapping();
$data_table_fields = $table_mapping->getFieldNames($new_data_table);
$base_table_fields = $table_mapping->getFieldNames($base_table);
$data_table = $new_data_table;
$this->processHandlers($all_views, function (&$handler_config, ViewEntityInterface $view) use ($entity_type_id, $base_table, $data_table, $base_table_fields, $data_table_fields) {
if (isset($handler_config['entity_type']) && isset($handler_config['entity_field']) && $handler_config['entity_type'] == $entity_type_id) {
// Move all fields which just exists on the data table.
if ($handler_config['table'] == $base_table && in_array($handler_config['entity_field'], $data_table_fields) && !in_array($handler_config['entity_field'], $base_table_fields)) {
$handler_config['table'] = $data_table;
$this->viewsToSave[$view->id()] = $view;
}
}
});
}
/**
* Updates views if a data table is removed.
*
* @param \Drupal\views\Entity\View[] $all_views
* All views.
* @param string $entity_type_id
* The entity type ID.
* @param string $old_data_table
* The name of the previous existing data table.
* @param string $base_table
* The name of the base table.
*/
protected function dataTableRemoval($all_views, $entity_type_id, $old_data_table, $base_table) {
// We move back the data table back to the base table.
$this->processHandlers($all_views, function (&$handler_config, ViewEntityInterface $view) use ($entity_type_id, $old_data_table, $base_table) {
if (isset($handler_config['entity_type']) && $handler_config['entity_type'] == $entity_type_id) {
if ($handler_config['table'] == $old_data_table) {
$handler_config['table'] = $base_table;
$this->viewsToSave[$view->id()] = $view;
}
}
});
}
/**
* Updates views if revision support is removed.
*
* @param \Drupal\views\Entity\View[] $all_views
* All views.
* @param \Drupal\Core\Entity\EntityTypeInterface $original
* The origin entity type.
*/
protected function revisionRemoval($all_views, EntityTypeInterface $original) {
$revision_base_table = $original->getRevisionTable();
$revision_data_table = $original->getRevisionDataTable();
foreach ($all_views as $view) {
if (in_array($view->get('base_table'), [$revision_base_table, $revision_data_table])) {
// Let's disable the views as we no longer support revisions.
$view->setStatus(FALSE);
$this->viewsToSave[$view->id()] = $view;
}
// For any kind of field, let's rely on the broken handler functionality.
}
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace Drupal\views\Exception;
/**
* Defines an exception for an invalid View render element.
*/
class ViewRenderElementException extends \Exception {}

View File

@ -0,0 +1,62 @@
<?php
namespace Drupal\views;
/**
* Caches exposed forms, as they are heavy to generate.
*
* @see \Drupal\views\Form\ViewsExposedForm
*/
class ExposedFormCache {
/**
* Stores the exposed form data.
*
* @var array
*/
protected $cache = [];
/**
* Save the Views exposed form for later use.
*
* @param string $view_id
* The views ID.
* @param string $display_id
* The current view display name.
* @param array $form_output
* The form structure. Only needed when inserting the value.
*/
public function setForm($view_id, $display_id, array $form_output) {
// Save the form output.
$views_exposed[$view_id][$display_id] = $form_output;
}
/**
* Retrieves the views exposed form from cache.
*
* @param string $view_id
* The views ID.
* @param string $display_id
* The current view display name.
*
* @return array|bool
* The form structure, if any, otherwise FALSE.
*/
public function getForm($view_id, $display_id) {
// Return the form output, if any.
if (empty($this->cache[$view_id][$display_id])) {
return FALSE;
}
else {
return $this->cache[$view_id][$display_id];
}
}
/**
* Rests the form cache.
*/
public function reset() {
$this->cache = [];
}
}

View File

@ -0,0 +1,81 @@
<?php
namespace Drupal\views;
use Drupal\Core\Field\BaseFieldDefinition;
/**
* A trait containing helper methods for field definitions.
*/
trait FieldAPIHandlerTrait {
/**
* The field definition.
*
* @var \Drupal\Core\Field\FieldDefinitionInterface
*/
protected $fieldDefinition;
/**
* The field storage definition.
*
* @var \Drupal\field\FieldStorageConfigInterface
*/
protected $fieldStorageDefinition;
/**
* The entity field manager.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected $entityFieldManager;
/**
* Gets the field definition.
*
* A View works on an entity type across bundles, and thus only has access to
* field storage definitions. In order to be able to use widgets and
* formatters, we create a generic field definition out of that storage
* definition.
*
* @see BaseFieldDefinition::createFromFieldStorageDefinition()
*
* @return \Drupal\Core\Field\FieldDefinitionInterface
* The field definition used by this handler.
*/
protected function getFieldDefinition() {
if (!$this->fieldDefinition) {
$field_storage_config = $this->getFieldStorageDefinition();
$this->fieldDefinition = BaseFieldDefinition::createFromFieldStorageDefinition($field_storage_config);
}
return $this->fieldDefinition;
}
/**
* Gets the field storage configuration.
*
* @return \Drupal\field\FieldStorageConfigInterface
* The field storage definition used by this handler
*/
protected function getFieldStorageDefinition() {
if (!$this->fieldStorageDefinition) {
$field_storage_definitions = $this->getEntityFieldManager()->getFieldStorageDefinitions($this->definition['entity_type']);
$this->fieldStorageDefinition = $field_storage_definitions[$this->definition['field_name']];
}
return $this->fieldStorageDefinition;
}
/**
* Returns the entity field manager.
*
* @return \Drupal\Core\Entity\EntityFieldManagerInterface
* The entity field manager.
*/
protected function getEntityFieldManager() {
if (!isset($this->entityFieldManager)) {
$this->entityFieldManager = \Drupal::service('entity_field.manager');
}
return $this->entityFieldManager;
}
}

View File

@ -0,0 +1,524 @@
<?php
namespace Drupal\views;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeManager;
use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
use Drupal\Core\Field\FieldTypePluginManagerInterface;
use Drupal\Core\Render\Markup;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\FieldStorageConfigInterface;
/**
* Provide default views data for fields.
*/
class FieldViewsDataProvider {
use StringTranslationTrait;
public function __construct(
protected readonly EntityTypeManager $entityTypeManager,
protected readonly FieldTypePluginManagerInterface $fieldTypePluginManager,
protected readonly EntityFieldManagerInterface $entityFieldManager,
) {}
/**
* Default views data implementation for a field.
*
* @param \Drupal\field\FieldStorageConfigInterface $field_storage
* The field definition.
*
* @return array
* The default views data for the field.
*/
public function defaultFieldImplementation(FieldStorageConfigInterface $field_storage): array {
$data = [];
// Check the field type is available.
if (!$this->fieldTypePluginManager->hasDefinition($field_storage->getType())) {
return $data;
}
// Check the field storage has fields.
if (!$field_storage->getBundles()) {
return $data;
}
// Ignore custom storage too.
if ($field_storage->hasCustomStorage()) {
return $data;
}
// Check whether the entity type storage is supported.
$storage = $this->getSqlStorageForField($field_storage);
if (!$storage) {
return $data;
}
$field_name = $field_storage->getName();
$field_columns = $field_storage->getColumns();
// Grab information about the entity type tables.
// We need to join to both the base table and the data table, if available.
$entity_type_id = $field_storage->getTargetEntityTypeId();
$entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
if (!$base_table = $entity_type->getBaseTable()) {
// We cannot do anything if for some reason there is no base table.
return $data;
}
$entity_tables = [$base_table => $entity_type_id];
// Some entities may not have a data table.
$data_table = $entity_type->getDataTable();
if ($data_table) {
$entity_tables[$data_table] = $entity_type_id;
}
$entity_revision_table = $entity_type->getRevisionTable();
$supports_revisions = $entity_type->hasKey('revision') && $entity_revision_table;
if ($supports_revisions) {
$entity_tables[$entity_revision_table] = $entity_type_id;
$entity_revision_data_table = $entity_type->getRevisionDataTable();
if ($entity_revision_data_table) {
$entity_tables[$entity_revision_data_table] = $entity_type_id;
}
}
// Description of the field tables.
// @todo Generalize this code to make it work with any table layout. See
// https://www.drupal.org/node/2079019.
$table_mapping = $storage->getTableMapping();
$field_tables = [
EntityStorageInterface::FIELD_LOAD_CURRENT => [
'table' => $table_mapping->getDedicatedDataTableName($field_storage),
'alias' => "{$entity_type_id}__{$field_name}",
],
];
if ($supports_revisions) {
$field_tables[EntityStorageInterface::FIELD_LOAD_REVISION] = [
'table' => $table_mapping->getDedicatedRevisionTableName($field_storage),
'alias' => "{$entity_type_id}_revision__{$field_name}",
];
}
// Determine if the fields are translatable.
$bundles_names = $field_storage->getBundles();
$translation_join_type = FALSE;
$fields = [];
$translatable_configs = [];
$untranslatable_configs = [];
$untranslatable_config_bundles = [];
foreach ($bundles_names as $bundle) {
$fields[$bundle] = FieldConfig::loadByName($entity_type->id(), $bundle, $field_name);
}
foreach ($fields as $bundle => $config_entity) {
if (!empty($config_entity)) {
if ($config_entity->isTranslatable()) {
$translatable_configs[$bundle] = $config_entity;
}
else {
$untranslatable_configs[$bundle] = $config_entity;
}
}
else {
// https://www.drupal.org/node/2451657#comment-11462881
\Drupal::logger('views')->error(
'A non-existent config entity name returned by FieldStorageConfigInterface::getBundles(): entity type: %entity_type, bundle: %bundle, field name: %field',
[
'%entity_type' => $entity_type->id(),
'%bundle' => $bundle,
'%field' => $field_name,
]
);
}
}
// If the field is translatable on all the bundles, there will be a join on
// the langcode.
if (!empty($translatable_configs) && empty($untranslatable_configs)) {
$translation_join_type = 'language';
}
// If the field is translatable only on certain bundles, there will be a
// join on langcode OR bundle name.
elseif (!empty($translatable_configs) && !empty($untranslatable_configs)) {
foreach ($untranslatable_configs as $config) {
$untranslatable_config_bundles[] = $config->getTargetBundle();
}
$translation_join_type = 'language_bundle';
}
// Build the relationships between the field table and the entity tables.
$table_alias = $field_tables[EntityStorageInterface::FIELD_LOAD_CURRENT]['alias'];
if ($data_table) {
// Tell Views how to join to the base table, via the data table.
$data[$table_alias]['table']['join'][$data_table] = [
'table' => $table_mapping->getDedicatedDataTableName($field_storage),
'left_field' => $entity_type->getKey('id'),
'field' => 'entity_id',
'extra' => [
['field' => 'deleted', 'value' => 0, 'numeric' => TRUE],
],
];
}
else {
// If there is no data table, just join directly.
$data[$table_alias]['table']['join'][$base_table] = [
'table' => $table_mapping->getDedicatedDataTableName($field_storage),
'left_field' => $entity_type->getKey('id'),
'field' => 'entity_id',
'extra' => [
['field' => 'deleted', 'value' => 0, 'numeric' => TRUE],
],
];
}
if ($translation_join_type === 'language_bundle') {
$data[$table_alias]['table']['join'][$data_table]['join_id'] = 'field_or_language_join';
$data[$table_alias]['table']['join'][$data_table]['extra'][] = [
'left_field' => 'langcode',
'field' => 'langcode',
];
$data[$table_alias]['table']['join'][$data_table]['extra'][] = [
'field' => 'bundle',
'value' => $untranslatable_config_bundles,
];
}
elseif ($translation_join_type === 'language') {
$data[$table_alias]['table']['join'][$data_table]['extra'][] = [
'left_field' => 'langcode',
'field' => 'langcode',
];
}
if ($supports_revisions) {
$table_alias = $field_tables[EntityStorageInterface::FIELD_LOAD_REVISION]['alias'];
if ($entity_revision_data_table) {
// Tell Views how to join to the revision table, via the data table.
$data[$table_alias]['table']['join'][$entity_revision_data_table] = [
'table' => $table_mapping->getDedicatedRevisionTableName($field_storage),
'left_field' => $entity_type->getKey('revision'),
'field' => 'revision_id',
'extra' => [
['field' => 'deleted', 'value' => 0, 'numeric' => TRUE],
],
];
}
else {
// If there is no data table, just join directly.
$data[$table_alias]['table']['join'][$entity_revision_table] = [
'table' => $table_mapping->getDedicatedRevisionTableName($field_storage),
'left_field' => $entity_type->getKey('revision'),
'field' => 'revision_id',
'extra' => [
['field' => 'deleted', 'value' => 0, 'numeric' => TRUE],
],
];
}
if ($translation_join_type === 'language_bundle') {
$data[$table_alias]['table']['join'][$entity_revision_data_table]['join_id'] = 'field_or_language_join';
$data[$table_alias]['table']['join'][$entity_revision_data_table]['extra'][] = [
'left_field' => 'langcode',
'field' => 'langcode',
];
$data[$table_alias]['table']['join'][$entity_revision_data_table]['extra'][] = [
'value' => $untranslatable_config_bundles,
'field' => 'bundle',
];
}
elseif ($translation_join_type === 'language') {
$data[$table_alias]['table']['join'][$entity_revision_data_table]['extra'][] = [
'left_field' => 'langcode',
'field' => 'langcode',
];
}
}
$group_name = $entity_type->getLabel();
// Get the list of bundles the field appears in.
$bundles_names = $field_storage->getBundles();
// Build the list of additional fields to add to queries.
$add_fields = ['delta', 'langcode', 'bundle'];
foreach (array_keys($field_columns) as $column) {
$add_fields[] = $table_mapping->getFieldColumnName($field_storage, $column);
}
// Determine the label to use for the field. We don't have a label available
// at the field level, so we just go through all fields and take the one
// which is used the most frequently.
[$label, $all_labels] = $this->entityFieldManager->getFieldLabels($entity_type_id, $field_name);
// Expose data for the field as a whole.
foreach ($field_tables as $type => $table_info) {
$table = $table_info['table'];
$table_alias = $table_info['alias'];
if ($type == EntityStorageInterface::FIELD_LOAD_CURRENT) {
$group = $group_name;
$field_alias = $field_name;
}
else {
$group = $this->t('@group (historical data)', ['@group' => $group_name]);
$field_alias = $field_name . '__revision_id';
}
$data[$table_alias][$field_alias] = [
'group' => $group,
'title' => $label,
'title short' => $label,
'help' => $this->t('Appears in: @bundles.', ['@bundles' => implode(', ', $bundles_names)]),
];
// Go through and create a list of aliases for all possible combinations
// of entity type + name.
$aliases = [];
$also_known = [];
foreach ($all_labels as $label_name => $true) {
if ($type == EntityStorageInterface::FIELD_LOAD_CURRENT) {
if ($label != $label_name) {
$aliases[] = [
'base' => $base_table,
'group' => $group_name,
'title' => $label_name,
'help' => $this->t('This is an alias of @group: @field.', ['@group' => $group_name, '@field' => $label]),
];
$also_known[] = $this->t('@group: @field', ['@group' => $group_name, '@field' => $label_name]);
}
}
elseif ($supports_revisions && $label != $label_name) {
$aliases[] = [
'base' => $table,
'group' => $this->t('@group (historical data)', ['@group' => $group_name]),
'title' => $label_name,
'help' => $this->t('This is an alias of @group: @field.', ['@group' => $group_name, '@field' => $label]),
];
$also_known[] = $this->t('@group (historical data): @field', ['@group' => $group_name, '@field' => $label_name]);
}
}
if ($aliases) {
$data[$table_alias][$field_alias]['aliases'] = $aliases;
// The $also_known variable contains markup that is HTML escaped and
// that loses safeness when imploded. The help text is used in
// #description and therefore XSS admin filtered by default. Escaped
// HTML is not altered by XSS filtering, therefore it is safe to just
// concatenate the strings. Afterwards we mark the entire string as
// safe, so it won't be escaped, no matter where it is used.
// Considering the dual use of this help data (both as metadata and as
// help text), other patterns such as use of #markup would not be
// correct here.
$data[$table_alias][$field_alias]['help'] = Markup::create($data[$table_alias][$field_alias]['help'] . ' ' . $this->t('Also known as:') . ' ' . implode(', ', $also_known));
}
$keys = array_keys($field_columns);
$real_field = reset($keys);
$data[$table_alias][$field_alias]['field'] = [
'table' => $table,
'id' => 'field',
'field_name' => $field_name,
'entity_type' => $entity_type_id,
// Provide a real field for group by.
'real field' => $field_name . '_' . $real_field,
'additional fields' => $add_fields,
// Default the element type to div, let the UI change it if necessary.
'element type' => 'div',
'is revision' => $type == EntityStorageInterface::FIELD_LOAD_REVISION,
];
}
// Expose data for each field property individually.
foreach ($field_columns as $column => $attributes) {
$allow_sort = TRUE;
// Identify likely filters and arguments for each column based on field
// type.
switch ($attributes['type']) {
case 'int':
case 'mediumint':
case 'tinyint':
case 'bigint':
case 'serial':
case 'numeric':
case 'float':
$filter = 'numeric';
$argument = 'numeric';
$sort = 'standard';
if ($field_storage->getType() == 'boolean') {
$filter = 'boolean';
}
break;
case 'blob':
// It does not make sense to sort by blob.
$allow_sort = FALSE;
default:
$filter = 'string';
$argument = 'string';
$sort = 'standard';
break;
}
if (count($field_columns) == 1 || $column == 'value') {
$title = $this->t('@label (@name)', ['@label' => $label, '@name' => $field_name]);
$title_short = $label;
}
else {
$title = $this->t('@label (@name:@column)', ['@label' => $label, '@name' => $field_name, '@column' => $column]);
$title_short = $this->t('@label:@column', ['@label' => $label, '@column' => $column]);
}
// Expose data for the property.
foreach ($field_tables as $type => $table_info) {
$table = $table_info['table'];
$table_alias = $table_info['alias'];
if ($type == EntityStorageInterface::FIELD_LOAD_CURRENT) {
$group = $group_name;
}
else {
$group = $this->t('@group (historical data)', ['@group' => $group_name]);
}
$column_real_name = $table_mapping->getFieldColumnName($field_storage, $column);
// Load all the fields from the table by default.
$additional_fields = $table_mapping->getAllColumns($table);
$data[$table_alias][$column_real_name] = [
'group' => $group,
'title' => $title,
'title short' => $title_short,
'help' => $this->t('Appears in: @bundles.', ['@bundles' => implode(', ', $bundles_names)]),
];
// Go through and create a list of aliases for all possible combinations
// of entity type + name.
$aliases = [];
$also_known = [];
foreach ($all_labels as $label_name => $true) {
if ($label != $label_name) {
if (count($field_columns) == 1 || $column == 'value') {
$alias_title = $this->t('@label (@name)', ['@label' => $label_name, '@name' => $field_name]);
}
else {
$alias_title = $this->t('@label (@name:@column)', ['@label' => $label_name, '@name' => $field_name, '@column' => $column]);
}
$aliases[] = [
'group' => $group_name,
'title' => $alias_title,
'help' => $this->t('This is an alias of @group: @field.', ['@group' => $group_name, '@field' => $title]),
];
$also_known[] = $this->t('@group: @field', ['@group' => $group_name, '@field' => $title]);
}
}
if ($aliases) {
$data[$table_alias][$column_real_name]['aliases'] = $aliases;
// The $also_known variable contains markup that is HTML escaped and
// that loses safeness when imploded. The help text is used in
// #description and therefore XSS admin filtered by default. Escaped
// HTML is not altered by XSS filtering, therefore it is safe to just
// concatenate the strings. Afterwards we mark the entire string as
// safe, so it won't be escaped, no matter where it is used.
// Considering the dual use of this help data (both as metadata and as
// help text), other patterns such as use of #markup would not be
// correct here.
$data[$table_alias][$column_real_name]['help'] = Markup::create($data[$table_alias][$column_real_name]['help'] . ' ' . $this->t('Also known as:') . ' ' . implode(', ', $also_known));
}
$data[$table_alias][$column_real_name]['argument'] = [
'field' => $column_real_name,
'table' => $table,
'id' => $argument,
'additional fields' => $additional_fields,
'field_name' => $field_name,
'entity_type' => $entity_type_id,
'empty field name' => $this->t('- No value -'),
];
$data[$table_alias][$column_real_name]['filter'] = [
'field' => $column_real_name,
'table' => $table,
'id' => $filter,
'additional fields' => $additional_fields,
'field_name' => $field_name,
'entity_type' => $entity_type_id,
'allow empty' => TRUE,
];
if (!empty($allow_sort)) {
$data[$table_alias][$column_real_name]['sort'] = [
'field' => $column_real_name,
'table' => $table,
'id' => $sort,
'additional fields' => $additional_fields,
'field_name' => $field_name,
'entity_type' => $entity_type_id,
];
}
// Set click sortable if there is a field definition.
if (isset($data[$table_alias][$field_name]['field'])) {
$data[$table_alias][$field_name]['field']['click sortable'] = $allow_sort;
}
// Expose additional delta column for multiple value fields.
if ($field_storage->isMultiple()) {
$title_delta = $this->t('@label (@name:delta)', ['@label' => $label, '@name' => $field_name]);
$title_short_delta = $this->t('@label:delta', ['@label' => $label]);
$data[$table_alias]['delta'] = [
'group' => $group,
'title' => $title_delta,
'title short' => $title_short_delta,
'help' => $this->t('Delta - Appears in: @bundles.', ['@bundles' => implode(', ', $bundles_names)]),
];
$data[$table_alias]['delta']['field'] = [
'id' => 'numeric',
];
$data[$table_alias]['delta']['argument'] = [
'field' => 'delta',
'table' => $table,
'id' => 'numeric',
'additional fields' => $additional_fields,
'empty field name' => $this->t('- No value -'),
'field_name' => $field_name,
'entity_type' => $entity_type_id,
];
$data[$table_alias]['delta']['filter'] = [
'field' => 'delta',
'table' => $table,
'id' => 'numeric',
'additional fields' => $additional_fields,
'field_name' => $field_name,
'entity_type' => $entity_type_id,
'allow empty' => TRUE,
];
$data[$table_alias]['delta']['sort'] = [
'field' => 'delta',
'table' => $table,
'id' => 'standard',
'additional fields' => $additional_fields,
'field_name' => $field_name,
'entity_type' => $entity_type_id,
];
}
}
}
return $data;
}
/**
* Determines whether the entity type the field appears in is SQL based.
*
* @param \Drupal\field\FieldStorageConfigInterface $field_storage
* The field storage definition.
*
* @return \Drupal\Core\Entity\Sql\SqlContentEntityStorage|bool
* Returns the entity type storage if supported and FALSE otherwise.
*/
public function getSqlStorageForField(FieldStorageConfigInterface $field_storage): SqlContentEntityStorage|bool {
$result = FALSE;
if ($this->entityTypeManager->hasDefinition($field_storage->getTargetEntityTypeId())) {
$storage = $this->entityTypeManager->getStorage($field_storage->getTargetEntityTypeId());
$result = $storage instanceof SqlContentEntityStorage ? $storage : FALSE;
}
return $result;
}
}

View File

@ -0,0 +1,220 @@
<?php
namespace Drupal\views\Form;
use Drupal\Component\Utility\Html;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\WorkspaceSafeFormInterface;
use Drupal\Core\Path\CurrentPathStack;
use Drupal\Core\Render\Element\Checkboxes;
use Drupal\Core\Url;
use Drupal\views\ExposedFormCache;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides the views exposed form.
*
* @internal
*/
class ViewsExposedForm extends FormBase implements WorkspaceSafeFormInterface {
/**
* The exposed form cache.
*
* @var \Drupal\views\ExposedFormCache
*/
protected $exposedFormCache;
/**
* The current path stack.
*
* @var \Drupal\Core\Path\CurrentPathStack
*/
protected $currentPathStack;
/**
* Constructs a new ViewsExposedForm.
*
* @param \Drupal\views\ExposedFormCache $exposed_form_cache
* The exposed form cache.
* @param \Drupal\Core\Path\CurrentPathStack $current_path_stack
* The current path stack.
*/
public function __construct(ExposedFormCache $exposed_form_cache, CurrentPathStack $current_path_stack) {
$this->exposedFormCache = $exposed_form_cache;
$this->currentPathStack = $current_path_stack;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('views.exposed_form_cache'),
$container->get('path.current')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'views_exposed_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
// Make sure that we validate because this form might be submitted
// multiple times per page.
$form_state->setValidationEnforced();
/** @var \Drupal\views\ViewExecutable $view */
$view = $form_state->get('view');
$display = &$form_state->get('display');
$form_state->setUserInput($view->getExposedInput());
// Let form plugins know this is for exposed widgets.
$form_state->set('exposed', TRUE);
// Check if the form was already created
if ($cache = $this->exposedFormCache->getForm($view->storage->id(), $view->current_display)) {
return $cache;
}
$form['#info'] = [];
// Go through each handler and let it generate its exposed widget.
foreach ($view->display_handler->handlers as $type => $value) {
/** @var \Drupal\views\Plugin\views\ViewsHandlerInterface $handler */
foreach ($view->$type as $id => $handler) {
if ($handler->canExpose() && $handler->isExposed()) {
// Grouped exposed filters have their own forms.
// Instead of render the standard exposed form, a new Select or
// Radio form field is rendered with the available groups.
// When a user chooses an option the selected value is split
// into the operator and value that the item represents.
if ($handler->isAGroup()) {
$handler->groupForm($form, $form_state);
$id = $handler->options['group_info']['identifier'];
}
else {
$handler->buildExposedForm($form, $form_state);
}
if ($info = $handler->exposedInfo()) {
$form['#info']["$type-$id"] = $info;
}
}
}
}
$form['actions'] = [
'#type' => 'actions',
];
$form['actions']['submit'] = [
// Prevent from showing up in \Drupal::request()->query.
'#name' => '',
'#type' => 'submit',
'#value' => $this->t('Apply'),
'#id' => Html::getUniqueId('edit-submit-' . $view->storage->id()),
];
if (!$view->hasUrl()) {
// On any non views.ajax route, use the current route for the form action.
if ($this->getRouteMatch()->getRouteName() !== 'views.ajax') {
$form_action = Url::fromRoute('<current>')->toString();
}
else {
// On the views.ajax route, set the action to the page we were on.
$form_action = Url::fromUserInput($this->currentPathStack->getPath())->toString();
}
}
else {
$form_action = $view->getUrl()->toString();
}
$form['#action'] = $form_action;
$form['#theme'] = $view->buildThemeFunctions('views_exposed_form');
$form['#id'] = Html::cleanCssIdentifier('views_exposed_form-' . $view->storage->id() . '-' . $display['id']);
// Labels are built too late for inline form errors to work, resulting
// in duplicated messages.
$form['#disable_inline_form_errors'] = TRUE;
/** @var \Drupal\views\Plugin\views\exposed_form\ExposedFormPluginInterface $exposed_form_plugin */
$exposed_form_plugin = $view->display_handler->getPlugin('exposed_form');
$exposed_form_plugin->exposedFormAlter($form, $form_state);
// Save the form.
$this->exposedFormCache->setForm($view->storage->id(), $view->current_display, $form);
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
$view = $form_state->get('view');
foreach (['field', 'filter'] as $type) {
/** @var \Drupal\views\Plugin\views\ViewsHandlerInterface[] $handlers */
$handlers = &$view->$type;
foreach ($handlers as $key => $handler) {
$handlers[$key]->validateExposed($form, $form_state);
}
}
/** @var \Drupal\views\Plugin\views\exposed_form\ExposedFormPluginInterface $exposed_form_plugin */
$exposed_form_plugin = $view->display_handler->getPlugin('exposed_form');
$exposed_form_plugin->exposedFormValidate($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// Form input keys that will not be included in $view->exposed_raw_data.
$exclude = ['submit', 'form_build_id', 'form_id', 'form_token', 'exposed_form_plugin', 'reset'];
$values = $form_state->getValues();
foreach (['field', 'filter'] as $type) {
/** @var \Drupal\views\Plugin\views\ViewsHandlerInterface[] $handlers */
$handlers = &$form_state->get('view')->$type;
foreach ($handlers as $key => $info) {
if ($handlers[$key]->acceptExposedInput($values)) {
$handlers[$key]->submitExposed($form, $form_state);
}
else {
// The input from the form did not validate, exclude it from the
// stored raw data.
$exclude[] = $key;
}
}
}
$view = $form_state->get('view');
$view->exposed_data = $values;
$view->exposed_raw_input = [];
/** @var \Drupal\views\Plugin\views\exposed_form\ExposedFormPluginBase $exposed_form_plugin */
$exposed_form_plugin = $view->display_handler->getPlugin('exposed_form');
$exposed_form_plugin->exposedFormSubmit($form, $form_state, $exclude);
foreach ($values as $key => $value) {
if (!empty($key) && !in_array($key, $exclude)) {
if (is_array($value)) {
// Handle checkboxes, we only want to include the checked options.
// @todo revisit the need for this when
// https://www.drupal.org/node/342316 is resolved.
$checked = Checkboxes::getCheckedCheckboxes($value);
foreach ($checked as $option_id) {
$view->exposed_raw_input[$key][$option_id] = $value[$option_id];
}
}
else {
$view->exposed_raw_input[$key] = $value;
}
}
}
}
}

View File

@ -0,0 +1,208 @@
<?php
namespace Drupal\views\Form;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\DependencyInjection\ClassResolverInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Drupal\Core\Form\FormInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\UrlGeneratorInterface;
use Drupal\Core\Url;
use Drupal\views\ViewExecutable;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* Provides a base class for single- or multistep view forms.
*
* This class only dispatches logic to the form for the current step. The form
* is always assumed to be multistep, even if it has only one step (which by
* default is \Drupal\views\Form\ViewsFormMainForm). That way it is actually
* possible for modules to have a multistep form if they need to.
*/
class ViewsForm implements FormInterface, ContainerInjectionInterface {
use DependencySerializationTrait;
/**
* The class resolver to get the subform form objects.
*
* @var \Drupal\Core\DependencyInjection\ClassResolverInterface
*/
protected $classResolver;
/**
* The request stack.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected $requestStack;
/**
* The URL generator to generate the form action.
*
* @var \Drupal\Core\Routing\UrlGeneratorInterface
*/
protected $urlGenerator;
/**
* The ID of the view.
*
* @var string
*/
protected $viewId;
/**
* The ID of the active view's display.
*
* @var string
*/
protected $viewDisplayId;
/**
* The arguments passed to the active view.
*
* @var string[]
*/
protected $viewArguments;
/**
* Constructs a ViewsForm object.
*
* @param \Drupal\Core\DependencyInjection\ClassResolverInterface $class_resolver
* The class resolver to get the subform form objects.
* @param \Drupal\Core\Routing\UrlGeneratorInterface $url_generator
* The URL generator to generate the form action.
* @param \Symfony\Component\HttpFoundation\RequestStack $requestStack
* The request stack.
* @param string $view_id
* The ID of the view.
* @param string $view_display_id
* The ID of the active view's display.
* @param string[] $view_args
* The arguments passed to the active view.
*/
public function __construct(ClassResolverInterface $class_resolver, UrlGeneratorInterface $url_generator, RequestStack $requestStack, $view_id, $view_display_id, array $view_args) {
$this->classResolver = $class_resolver;
$this->urlGenerator = $url_generator;
$this->requestStack = $requestStack;
$this->viewId = $view_id;
$this->viewDisplayId = $view_display_id;
$this->viewArguments = $view_args;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $view_id = NULL, $view_display_id = NULL, ?array $view_args = NULL) {
return new static(
$container->get('class_resolver'),
$container->get('url_generator'),
$container->get('request_stack'),
$view_id,
$view_display_id,
$view_args
);
}
/**
* Returns a string for the form's base ID.
*
* @return string
* The string identifying the form's base ID.
*/
public function getBaseFormId() {
$parts = [
'views_form',
$this->viewId,
$this->viewDisplayId,
];
return implode('_', $parts);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
$parts = [
$this->getBaseFormId(),
];
if (!empty($this->viewArguments)) {
// Append the passed arguments to ensure form uniqueness.
$parts = array_merge($parts, $this->viewArguments);
}
return implode('_', $parts);
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, ?ViewExecutable $view = NULL, $output = []) {
if (!$step = $form_state->get('step')) {
$step = 'views_form_views_form';
$form_state->set('step', $step);
}
$form_state->set(['step_controller', 'views_form_views_form'], 'Drupal\views\Form\ViewsFormMainForm');
// Views forms without view arguments return the same Base Form ID and
// Form ID. Base form ID should only be added when different.
if ($this->getBaseFormId() !== $this->getFormId()) {
$form_state->addBuildInfo('base_form_id', $this->getBaseFormId());
}
$form = [];
$query = $this->requestStack->getCurrentRequest()->query->all();
$query = UrlHelper::filterQueryParameters($query, ['_wrapper_format'], '');
$options = ['query' => $query];
$form['#action'] = $view->hasUrl() ? $view->getUrl()->setOptions($options)->toString() : Url::fromRoute('<current>')->setOptions($options)->toString();
// Tell the preprocessor whether it should hide the header, footer, pager,
// etc.
$form['show_view_elements'] = [
'#type' => 'value',
'#value' => ($step == 'views_form_views_form') ? TRUE : FALSE,
];
$form_object = $this->getFormObject($form_state);
$form += $form_object->buildForm($form, $form_state, $view, $output);
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
$form_object = $this->getFormObject($form_state);
$form_object->validateForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$form_object = $this->getFormObject($form_state);
$form_object->submitForm($form, $form_state);
}
/**
* Returns the object used to build the step form.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form_state of the current form.
*
* @return \Drupal\Core\Form\FormInterface
* The form object to use.
*/
protected function getFormObject(FormStateInterface $form_state) {
// If this is a class, instantiate it.
$form_step_class = $form_state->get(['step_controller', $form_state->get('step')]) ?: 'Drupal\views\Form\ViewsFormMainForm';
return $this->classResolver->getInstanceFromDefinition($form_step_class);
}
}

View File

@ -0,0 +1,210 @@
<?php
namespace Drupal\views\Form;
use Drupal\Component\Render\MarkupInterface;
use Drupal\Component\Utility\Html;
use Drupal\Core\Form\FormInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\WorkspaceDynamicSafeFormInterface;
use Drupal\Core\Form\WorkspaceSafeFormInterface;
use Drupal\Core\Security\TrustedCallbackInterface;
use Drupal\views\Render\ViewsRenderPipelineMarkup;
use Drupal\views\ViewExecutable;
use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
* Provides a default main form class for Views forms.
*/
class ViewsFormMainForm implements FormInterface, TrustedCallbackInterface {
use StringTranslationTrait;
/**
* {@inheritdoc}
*/
public function getFormId() {
return '';
}
/**
* Replaces views substitution placeholders.
*
* @param array $element
* An associative array containing the properties of the element.
* Properties used: #substitutions, #children.
*
* @return array
* The $element with prepared variables ready for #theme 'form'
* in views_form_views_form.
*/
public static function preRenderViewsForm(array $element) {
// Placeholders and their substitutions (usually rendered form elements).
$search = [];
$replace = [];
// Add in substitutions provided by the form.
foreach ($element['#substitutions']['#value'] as $substitution) {
$field_name = $substitution['field_name'];
$row_id = $substitution['row_id'];
$search[] = $substitution['placeholder'];
$replace[] = isset($element[$field_name][$row_id]) ? \Drupal::service('renderer')->render($element[$field_name][$row_id]) : '';
}
// Add in substitutions from hook_views_form_substitutions().
$substitutions = \Drupal::moduleHandler()->invokeAll('views_form_substitutions');
foreach ($substitutions as $placeholder => $substitution) {
$search[] = Html::escape($placeholder);
// Ensure that any replacements made are safe to make.
if (!($substitution instanceof MarkupInterface)) {
$substitution = Html::escape($substitution);
}
$replace[] = $substitution;
}
// Apply substitutions to the rendered output.
$output = str_replace($search, $replace, \Drupal::service('renderer')->render($element['output']));
$element['output'] = ['#markup' => ViewsRenderPipelineMarkup::create($output)];
return $element;
}
/**
* {@inheritdoc}
*/
public static function trustedCallbacks() {
return ['preRenderViewsForm'];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, ?ViewExecutable $view = NULL, $output = []) {
$form['#prefix'] = '<div class="views-form">';
$form['#suffix'] = '</div>';
$form['#pre_render'][] = [static::class, 'preRenderViewsForm'];
// Add the output markup to the form array so that it's included when the
// form array is passed to the theme function.
$form['output'] = $output;
// This way any additional form elements will go before the view
// (below the exposed widgets).
$form['output']['#weight'] = 50;
$form['actions'] = [
'#type' => 'actions',
];
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Save'),
];
$substitutions = [];
foreach ($view->field as $field_name => $field) {
$form_element_name = $field_name;
if (method_exists($field, 'form_element_name')) {
$form_element_name = $field->form_element_name();
}
$method_form_element_row_id_exists = FALSE;
if (method_exists($field, 'form_element_row_id')) {
$method_form_element_row_id_exists = TRUE;
}
// If the field provides a views form, allow it to modify the $form array.
$has_form = FALSE;
if (method_exists($field, 'viewsForm')) {
$field->viewsForm($form, $form_state);
// Allow the views form to determine whether it's safe to be submitted
// in a workspace.
$workspace_safe = $field instanceof WorkspaceSafeFormInterface
|| ($field instanceof WorkspaceDynamicSafeFormInterface && $field->isWorkspaceSafeForm($form, $form_state));
$form_state->set('workspace_safe', $workspace_safe);
$has_form = TRUE;
}
// Build the substitutions array for use in the theme function.
if ($has_form) {
foreach ($view->result as $row_id => $row) {
if ($method_form_element_row_id_exists) {
$form_element_row_id = $field->form_element_row_id($row_id);
}
else {
$form_element_row_id = $row_id;
}
$substitutions[] = [
'placeholder' => '<!--form-item-' . $form_element_name . '--' . $form_element_row_id . '-->',
'field_name' => $form_element_name,
'row_id' => $form_element_row_id,
];
}
}
}
// Give the area handlers a chance to extend the form.
$area_handlers = array_merge(array_values($view->header), array_values($view->footer));
$empty = empty($view->result);
foreach ($area_handlers as $area) {
if (method_exists($area, 'viewsForm') && !$area->viewsFormEmpty($empty)) {
$area->viewsForm($form, $form_state);
}
}
$form['#substitutions'] = [
'#type' => 'value',
'#value' => $substitutions,
];
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
$view = $form_state->getBuildInfo()['args'][0];
// Call the validation method on every field handler that has it.
foreach ($view->field as $field) {
if (method_exists($field, 'viewsFormValidate')) {
$field->viewsFormValidate($form, $form_state);
}
}
// Call the validate method on every area handler that has it.
foreach (['header', 'footer'] as $area) {
foreach ($view->{$area} as $area_handler) {
if (method_exists($area_handler, 'viewsFormValidate')) {
$area_handler->viewsFormValidate($form, $form_state);
}
}
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$view = $form_state->getBuildInfo()['args'][0];
// Call the submit method on every field handler that has it.
foreach ($view->field as $field) {
if (method_exists($field, 'viewsFormSubmit')) {
$field->viewsFormSubmit($form, $form_state);
}
}
// Call the submit method on every area handler that has it.
foreach (['header', 'footer'] as $area) {
foreach ($view->{$area} as $area_handler) {
if (method_exists($area_handler, 'viewsFormSubmit')) {
$area_handler->viewsFormSubmit($form, $form_state);
}
}
}
}
}

View File

@ -0,0 +1,397 @@
<?php
namespace Drupal\views\Hook;
use Drupal\block\BlockInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\views\ViewsConfigUpdater;
use Drupal\views\ViewEntityInterface;
use Drupal\views\Plugin\Derivative\ViewsLocalTask;
use Drupal\Core\Database\Query\AlterableInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\views\ViewExecutable;
use Drupal\views\Views;
use Drupal\Component\Utility\Html;
use Drupal\Core\Url;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Hook\Attribute\Hook;
/**
* Hook implementations for views.
*/
class ViewsHooks {
use StringTranslationTrait;
/**
* Implements hook_help().
*/
#[Hook('help')]
public function help($route_name, RouteMatchInterface $route_match): ?string {
switch ($route_name) {
case 'help.page.views':
$output = '';
$output .= '<h2>' . $this->t('About') . '</h2>';
$output .= '<p>' . $this->t('The Views module provides a back end to fetch information from content, user accounts, taxonomy terms, and other entities from the database and present it to the user as a grid, HTML list, table, unformatted list, etc. The resulting displays are known generally as <em>views</em>.') . '</p>';
$output .= '<p>' . $this->t('For more information, see the <a href=":views">online documentation for the Views module</a>.', [':views' => 'https://www.drupal.org/documentation/modules/views']) . '</p>';
$output .= '<p>' . $this->t('In order to create and modify your own views using the administration and configuration user interface, you will need to install either the Views UI module in core or a contributed module that provides a user interface for Views. See the <a href=":views-ui">Views UI module help page</a> for more information.', [
':views-ui' => \Drupal::moduleHandler()->moduleExists('views_ui') ? Url::fromRoute('help.page', [
'name' => 'views_ui',
])->toString() : '#',
]) . '</p>';
$output .= '<h2>' . $this->t('Uses') . '</h2>';
$output .= '<dl>';
$output .= '<dt>' . $this->t('Adding functionality to administrative pages') . '</dt>';
$output .= '<dd>' . $this->t('The Views module adds functionality to some core administration pages. For example, <em>admin/content</em> uses Views to filter and sort content. With Views uninstalled, <em>admin/content</em> is more limited.') . '</dd>';
$output .= '<dt>' . $this->t('Expanding Views functionality') . '</dt>';
$output .= '<dd>' . $this->t('Contributed projects that support the Views module can be found in the <a href=":node">online documentation for Views-related contributed modules</a>.', [':node' => 'https://www.drupal.org/documentation/modules/views/add-ons']) . '</dd>';
$output .= '<dt>' . $this->t('Improving table accessibility') . '</dt>';
$output .= '<dd>' . $this->t('Views tables include semantic markup to improve accessibility. Data cells are automatically associated with header cells through id and header attributes. To improve the accessibility of your tables you can add descriptive elements within the Views table settings. The <em>caption</em> element can introduce context for a table, making it easier to understand. The <em>summary</em> element can provide an overview of how the data has been organized and how to navigate the table. Both the caption and summary are visible by default and also implemented according to HTML5 guidelines.') . '</dd>';
$output .= '<dt>' . $this->t('Working with multilingual views') . '</dt>';
$output .= '<dd>' . $this->t('If your site has multiple languages and translated entities, each result row in a view will contain one translation of each involved entity (a view can involve multiple entities if it uses relationships). You can use a filter to restrict your view to one language: without filtering, if an entity has three translations it will add three rows to the results; if you filter by language, at most one result will appear (it could be zero if that particular entity does not have a translation matching your language filter choice). If a view uses relationships, each entity in the relationship needs to be filtered separately. You can filter a view to a fixed language choice, such as English or Spanish, or to the language selected by the page the view is displayed on (the language that is selected for the page by the language detection settings either for Content or User interface).') . '</dd>';
$output .= '<dd>' . $this->t('Because each result row contains a specific translation of each entity, field-level filters are also relative to these entity translations. For example, if your view has a filter that specifies that the entity title should contain a particular English word, you will presumably filter out all rows containing Chinese translations, since they will not contain the English word. If your view also has a second filter specifying that the title should contain a particular Chinese word, and if you are using "And" logic for filtering, you will presumably end up with no results in the view, because there are probably not any entity translations containing both the English and Chinese words in the title.') . '</dd>';
$output .= '<dd>' . $this->t('Independent of filtering, you can choose the display language (the language used to display the entities and their fields) via a setting on the display. Your language choices are the same as the filter language choices, with an additional choice of "Content language of view row" and "Original language of content in view row", which means to display each entity in the result row using the language that entity has or in which it was originally created. In theory, this would give you the flexibility to filter to French translations, for instance, and then display the results in Spanish. The more usual choices would be to use the same language choices for the display language and each entity filter in the view, or to use the Row language setting for the display.') . '</dd>';
$output .= '</dl>';
return $output;
}
return NULL;
}
/**
* Implements hook_views_pre_render().
*/
#[Hook('views_pre_render')]
public function viewsPreRender($view): void {
// If using AJAX, send identifying data about this view.
if ($view->ajaxEnabled() && empty($view->is_attachment) && empty($view->live_preview)) {
$view->element['#attached']['drupalSettings']['views'] = [
'ajax_path' => Url::fromRoute('views.ajax')->toString(),
'ajaxViews' => [
'views_dom_id:' . $view->dom_id => [
'view_name' => $view->storage->id(),
'view_display_id' => $view->current_display,
'view_args' => Html::escape(implode('/', $view->args)),
'view_path' => Html::escape(\Drupal::service('path.current')->getPath()),
'view_base_path' => $view->getPath(),
'view_dom_id' => $view->dom_id,
// To fit multiple views on a page, the programmer may have
// overridden the display's pager_element.
'pager_element' => isset($view->pager) ? $view->pager->getPagerId() : 0,
],
],
];
$view->element['#attached']['library'][] = 'views/views.ajax';
}
}
/**
* Implements hook_theme().
*
* Register views theming functions and those that are defined via views
* plugin definitions.
*/
#[Hook('theme')]
public function theme($existing, $type, $theme, $path) : array {
\Drupal::moduleHandler()->loadInclude('views', 'inc', 'views.theme');
// Some quasi clever array merging here.
$base = ['file' => 'views.theme.inc'];
// Our extra version of pager
$hooks['views_mini_pager'] = $base + [
'variables' => [
'tags' => [],
'quantity' => 9,
'element' => 0,
'pagination_heading_level' => 'h4',
'parameters' => [],
],
];
$variables = [
// For displays, we pass in a dummy array as the first parameter, since
// $view is an object but the core contextual_preprocess() function only
// attaches contextual links when the primary theme argument is an array.
'display' => [
'view_array' => [],
'view' => NULL,
'rows' => [],
'header' => [],
'footer' => [],
'empty' => [],
'exposed' => [],
'more' => [],
'feed_icons' => [],
'pager' => [],
'title' => '',
'attachment_before' => [],
'attachment_after' => [],
],
'style' => [
'view' => NULL,
'options' => NULL,
'rows' => NULL,
'title' => NULL,
],
'row' => [
'view' => NULL,
'options' => NULL,
'row' => NULL,
'field_alias' => NULL,
],
'exposed_form' => [
'view' => NULL,
'options' => NULL,
],
'pager' => [
'view' => NULL,
'options' => NULL,
'tags' => [],
'quantity' => 9,
'element' => 0,
'pagination_heading_level' => 'h4',
'parameters' => [],
],
];
// Default view themes
$hooks['views_view_field'] = $base + ['variables' => ['view' => NULL, 'field' => NULL, 'row' => NULL]];
$hooks['views_view_grouping'] = $base + [
'variables' => [
'view' => NULL,
'grouping' => NULL,
'grouping_level' => NULL,
'rows' => NULL,
'title' => NULL,
],
];
// Only display, pager, row, and style plugins can provide theme hooks.
$plugin_types = ['display', 'pager', 'row', 'style', 'exposed_form'];
$plugins = [];
foreach ($plugin_types as $plugin_type) {
$plugins[$plugin_type] = Views::pluginManager($plugin_type)->getDefinitions();
}
$module_handler = \Drupal::moduleHandler();
// Register theme functions for all style plugins. It provides a basic auto
// implementation of theme functions or template files by using the plugin
// definitions (theme, theme_file, module, register_theme). Template files
// are assumed to be located in the templates folder.
foreach ($plugins as $type => $info) {
foreach ($info as $def) {
// Not all plugins have theme functions, and they can also explicitly
// prevent a theme function from being registered automatically.
if (!isset($def['theme']) || empty($def['register_theme'])) {
continue;
}
// For each theme registration, we have a base directory to check for
// the templates folder. This will be relative to the root of the given
// module folder, so we always need a module definition.
// @todo Watchdog or exception?
if (!isset($def['provider']) || !$module_handler->moduleExists($def['provider'])) {
continue;
}
$hooks[$def['theme']] = ['variables' => $variables[$type]];
// We always use the module directory as base dir.
$module_dir = \Drupal::service('extension.list.module')->getPath($def['provider']);
$hooks[$def['theme']]['path'] = $module_dir;
// For the views module we ensure views.theme.inc is included.
if ($def['provider'] == 'views') {
if (!isset($hooks[$def['theme']]['includes'])) {
$hooks[$def['theme']]['includes'] = [];
}
if (!in_array('views.theme.inc', $hooks[$def['theme']]['includes'])) {
$hooks[$def['theme']]['includes'][] = $module_dir . '/views.theme.inc';
}
}
elseif (!empty($def['theme_file'])) {
$hooks[$def['theme']]['file'] = $def['theme_file'];
}
// Whenever we have a theme file, we include it directly so we can
// auto-detect the theme function.
if (isset($def['theme_file'])) {
$include = \Drupal::root() . '/' . $module_dir . '/' . $def['theme_file'];
if (is_file($include)) {
require_once $include;
}
}
// By default any templates for a module are located in the /templates
// directory of the module's folder. If a module wants to define its own
// location it has to set register_theme of the plugin to FALSE and
// implement hook_theme() by itself.
$hooks[$def['theme']]['path'] .= '/templates';
$hooks[$def['theme']]['template'] = Html::cleanCssIdentifier($def['theme']);
}
}
$hooks['views_form_views_form'] = $base + ['render element' => 'form'];
$hooks['views_exposed_form'] = $base + ['render element' => 'form'];
return $hooks;
}
/**
* Implements hook_theme_suggestions_HOOK_alter().
*/
#[Hook('theme_suggestions_node_alter')]
public function themeSuggestionsNodeAlter(array &$suggestions, array $variables): void {
$node = $variables['elements']['#node'];
if (!empty($node->view) && $node->view->storage->id()) {
$suggestions[] = 'node__view__' . $node->view->storage->id();
if (!empty($node->view->current_display)) {
$suggestions[] = 'node__view__' . $node->view->storage->id() . '__' . $node->view->current_display;
}
}
}
/**
* Implements hook_theme_suggestions_HOOK_alter().
*/
#[Hook('theme_suggestions_comment_alter')]
public function themeSuggestionsCommentAlter(array &$suggestions, array $variables): void {
$comment = $variables['elements']['#comment'];
if (!empty($comment->view) && $comment->view->storage->id()) {
$suggestions[] = 'comment__view__' . $comment->view->storage->id();
if (!empty($comment->view->current_display)) {
$suggestions[] = 'comment__view__' . $comment->view->storage->id() . '__' . $comment->view->current_display;
}
}
}
/**
* Implements hook_theme_suggestions_HOOK_alter().
*/
#[Hook('theme_suggestions_container_alter')]
public function themeSuggestionsContainerAlter(array &$suggestions, array $variables): void {
if (!empty($variables['element']['#type']) && $variables['element']['#type'] == 'more_link' && !empty($variables['element']['#view']) && $variables['element']['#view'] instanceof ViewExecutable) {
$suggestions = array_merge(
$suggestions,
// Theme suggestions use the reverse order compared to #theme hooks.
array_reverse($variables['element']['#view']->buildThemeFunctions('container__more_link'))
);
}
}
/**
* Implements hook_ENTITY_TYPE_insert() for 'field_config'.
*/
#[Hook('field_config_insert')]
public function fieldConfigInsert(EntityInterface $field): void {
Views::viewsData()->clear();
}
/**
* Implements hook_ENTITY_TYPE_update() for 'field_config'.
*/
#[Hook('field_config_update')]
public function fieldConfigUpdate(EntityInterface $entity): void {
Views::viewsData()->clear();
}
/**
* Implements hook_ENTITY_TYPE_delete() for 'field_config'.
*/
#[Hook('field_config_delete')]
public function fieldConfigDelete(EntityInterface $entity): void {
Views::viewsData()->clear();
}
/**
* Implements hook_ENTITY_TYPE_insert().
*/
#[Hook('base_field_override_insert')]
public function baseFieldOverrideInsert(EntityInterface $entity): void {
Views::viewsData()->clear();
}
/**
* Implements hook_ENTITY_TYPE_update().
*/
#[Hook('base_field_override_update')]
public function baseFieldOverrideUpdate(EntityInterface $entity): void {
Views::viewsData()->clear();
}
/**
* Implements hook_ENTITY_TYPE_delete().
*/
#[Hook('base_field_override_delete')]
public function baseFieldOverrideDelete(EntityInterface $entity): void {
Views::viewsData()->clear();
}
/**
* Implements hook_form_FORM_ID_alter() for the exposed form.
*
* Since the exposed form is a GET form, we don't want it to send a wide
* variety of information.
*/
#[Hook('form_views_exposed_form_alter')]
public function formViewsExposedFormAlter(&$form, FormStateInterface $form_state) : void {
$form['form_build_id']['#access'] = FALSE;
$form['form_token']['#access'] = FALSE;
$form['form_id']['#access'] = FALSE;
}
/**
* Implements hook_query_TAG_alter().
*
* This is the hook_query_alter() for queries tagged by Views and is used to
* add in substitutions from hook_views_query_substitutions().
*/
#[Hook('query_views_alter')]
public function queryViewsAlter(AlterableInterface $query): void {
$substitutions = $query->getMetaData('views_substitutions');
$tables =& $query->getTables();
$where =& $query->conditions();
// Replaces substitutions in tables.
foreach ($tables as $table_name => $table_metadata) {
foreach ($table_metadata['arguments'] as $replacement_key => $value) {
if (!is_array($value)) {
if (isset($substitutions[$value])) {
$tables[$table_name]['arguments'][$replacement_key] = $substitutions[$value];
}
}
else {
foreach ($value as $sub_key => $sub_value) {
if (isset($substitutions[$sub_value])) {
$tables[$table_name]['arguments'][$replacement_key][$sub_key] = $substitutions[$sub_value];
}
}
}
}
}
// Replaces substitutions in filter criteria.
_views_query_tag_alter_condition($query, $where, $substitutions);
}
/**
* Implements hook_local_tasks_alter().
*/
#[Hook('local_tasks_alter')]
public function localTasksAlter(&$local_tasks): void {
$container = \Drupal::getContainer();
$local_task = ViewsLocalTask::create($container, 'views_view');
$local_task->alterLocalTasks($local_tasks);
}
/**
* Implements hook_ENTITY_TYPE_presave().
*/
#[Hook('view_presave')]
public function viewPresave(ViewEntityInterface $view): void {
/** @var \Drupal\views\ViewsConfigUpdater $config_updater */
$config_updater = \Drupal::classResolver(ViewsConfigUpdater::class);
$config_updater->updateAll($view);
}
/**
* Implements hook_ENTITY_TYPE_presave() for blocks.
*/
#[Hook('block_presave')]
public function blockPresave(BlockInterface $block): void {
if (str_starts_with($block->getPluginId(), 'views_block:')) {
$settings = $block->get('settings');
if (isset($settings['items_per_page']) && $settings['items_per_page'] === 'none') {
@trigger_error('Saving a views block with "none" items per page is deprecated in drupal:11.2.0 and removed in drupal:12.0.0. To use the items per page defined by the view, use NULL. See https://www.drupal.org/node/3522240', E_USER_DEPRECATED);
$settings['items_per_page'] = NULL;
$block->set('settings', $settings);
}
}
}
}

View File

@ -0,0 +1,136 @@
<?php
namespace Drupal\views\Hook;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\Core\Hook\Attribute\Hook;
use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
* Hook implementations for views.
*/
class ViewsTokensHooks {
use StringTranslationTrait;
/**
* Implements hook_token_info().
*/
#[Hook('token_info')]
public function tokenInfo(): array {
$info['types']['view'] = [
'name' => $this->t('View', [], [
'context' => 'View entity type',
]),
'description' => $this->t('Tokens related to views.'),
'needs-data' => 'view',
];
$info['tokens']['view']['label'] = ['name' => $this->t('Label'), 'description' => $this->t('The label of the view.')];
$info['tokens']['view']['description'] = ['name' => $this->t('Description'), 'description' => $this->t('The description of the view.')];
$info['tokens']['view']['id'] = ['name' => $this->t('ID'), 'description' => $this->t('The machine-readable ID of the view.')];
$info['tokens']['view']['title'] = [
'name' => $this->t('Title'),
'description' => $this->t('The title of current display of the view.'),
];
$info['tokens']['view']['url'] = ['name' => $this->t('URL'), 'description' => $this->t('The URL of the view.'), 'type' => 'url'];
$info['tokens']['view']['base-table'] = [
'name' => $this->t('Base table'),
'description' => $this->t('The base table used for this view.'),
];
$info['tokens']['view']['base-field'] = [
'name' => $this->t('Base field'),
'description' => $this->t('The base field used for this view.'),
];
$info['tokens']['view']['total-rows'] = [
'name' => $this->t('Total rows'),
'description' => $this->t('The total amount of results returned from the view. The current display will be used.'),
];
$info['tokens']['view']['items-per-page'] = [
'name' => $this->t('Items per page'),
'description' => $this->t('The number of items per page.'),
];
$info['tokens']['view']['current-page'] = [
'name' => $this->t('Current page'),
'description' => $this->t('The current page of results the view is on.'),
];
$info['tokens']['view']['page-count'] = ['name' => $this->t('Page count'), 'description' => $this->t('The total page count.')];
return $info;
}
/**
* Implements hook_tokens().
*/
#[Hook('tokens')]
public function tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata): array {
$url_options = ['absolute' => TRUE];
if (isset($options['language'])) {
$url_options['language'] = $options['language'];
}
$replacements = [];
if ($type == 'view' && !empty($data['view'])) {
/** @var \Drupal\views\ViewExecutable $view */
$view = $data['view'];
$bubbleable_metadata->addCacheableDependency($view->storage);
foreach ($tokens as $name => $original) {
switch ($name) {
case 'label':
$replacements[$original] = $view->storage->label();
break;
case 'description':
$replacements[$original] = $view->storage->get('description');
break;
case 'id':
$replacements[$original] = $view->storage->id();
break;
case 'title':
$title = $view->getTitle();
$replacements[$original] = $title;
break;
case 'url':
try {
if ($url = $view->getUrl()) {
$replacements[$original] = $url->setOptions($url_options)->toString();
}
}
catch (\InvalidArgumentException) {
// The view has no URL so we leave the value empty.
$replacements[$original] = '';
}
break;
case 'base-table':
$replacements[$original] = $view->storage->get('base_table');
break;
case 'base-field':
$replacements[$original] = $view->storage->get('base_field');
break;
case 'total-rows':
$replacements[$original] = (int) $view->total_rows;
break;
case 'items-per-page':
$replacements[$original] = (int) $view->getItemsPerPage();
break;
case 'current-page':
$replacements[$original] = (int) $view->getCurrentPage() + 1;
break;
case 'page-count':
// If there are no items per page, set this to 1 for the division.
$per_page = $view->getItemsPerPage() ?: 1;
$replacements[$original] = max(1, (int) ceil($view->total_rows / $per_page));
break;
}
}
}
return $replacements;
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace Drupal\views\Hook;
use Drupal\views\Plugin\views\PluginBase;
use Drupal\views\ViewExecutable;
use Drupal\Core\Hook\Attribute\Hook;
/**
* Hook implementations for views.
*/
class ViewsViewsExecutionHooks {
/**
* Implements hook_views_query_substitutions().
*
* Makes the following substitutions:
* - Current time.
* - Drupal version.
* - Special language codes; see
* \Drupal\views\Plugin\views\PluginBase::listLanguages().
*/
#[Hook('views_query_substitutions')]
public function viewsQuerySubstitutions(ViewExecutable $view): array {
$substitutions = [
'***CURRENT_VERSION***' => \Drupal::VERSION,
'***CURRENT_TIME***' => \Drupal::time()->getRequestTime(),
] + PluginBase::queryLanguageSubstitutions();
return $substitutions;
}
/**
* Implements hook_views_form_substitutions().
*/
#[Hook('views_form_substitutions')]
public function viewsFormSubstitutions(): array {
$select_all = [
'#type' => 'checkbox',
'#default_value' => FALSE,
'#attributes' => [
'class' => [
'action-table-select-all',
],
],
];
return [
'<!--action-bulk-form-select-all-->' => \Drupal::service('renderer')->render($select_all),
];
}
}

View File

@ -0,0 +1,274 @@
<?php
namespace Drupal\views\Hook;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\field\FieldStorageConfigInterface;
use Drupal\Component\Utility\NestedArray;
use Drupal\system\ActionConfigEntityInterface;
use Drupal\Core\Hook\Attribute\Hook;
/**
* Hook implementations for views.
*/
class ViewsViewsHooks {
use StringTranslationTrait;
/**
* Implements hook_views_data().
*/
#[Hook('views_data')]
public function viewsData(): array {
$data['views']['table']['group'] = $this->t('Global');
$data['views']['table']['join'] = ['#global' => []];
$data['views']['random'] = [
'title' => $this->t('Random'),
'help' => $this->t('Randomize the display order.'),
'sort' => [
'id' => 'random',
],
];
$data['views']['null'] = [
'title' => $this->t('Null'),
'help' => $this->t('Allow a contextual filter value to be ignored. The query will not be altered by this contextual filter value. Can be used when contextual filter values come from the URL, and a part of the URL needs to be ignored.'),
'argument' => [
'id' => 'null',
],
];
$data['views']['nothing'] = [
'title' => $this->t('Custom text'),
'help' => $this->t('Provide custom text or link.'),
'field' => [
'id' => 'custom',
'click sortable' => FALSE,
],
];
$data['views']['counter'] = [
'title' => $this->t('View result counter'),
'help' => $this->t('Displays the actual position of the view result'),
'field' => [
'id' => 'counter',
],
];
$data['views']['area'] = [
'title' => $this->t('Text area'),
'help' => $this->t('Provide markup for the area using any available text format.'),
'area' => [
'id' => 'text',
],
];
$data['views']['area_text_custom'] = [
'title' => $this->t('Unfiltered text'),
'help' => $this->t('Provide markup for the area with minimal filtering.'),
'area' => [
'id' => 'text_custom',
],
];
$data['views']['title'] = [
'title' => $this->t('Title override'),
'help' => $this->t('Override the default view title for this view. This is useful to display an alternative title when a view is empty.'),
'area' => [
'id' => 'title',
'sub_type' => 'empty',
],
];
$data['views']['view'] = [
'title' => $this->t('View area'),
'help' => $this->t('Insert a view inside an area.'),
'area' => [
'id' => 'view',
],
];
$data['views']['result'] = [
'title' => $this->t('Result summary'),
'help' => $this->t('Shows result summary, for example the items per page.'),
'area' => [
'id' => 'result',
],
];
$data['views']['messages'] = [
'title' => $this->t('Messages'),
'help' => $this->t('Displays messages in an area.'),
'area' => [
'id' => 'messages',
],
];
$data['views']['http_status_code'] = [
'title' => $this->t('Response status code'),
'help' => $this->t('Alter the HTTP response status code used by this view, mostly helpful for empty results.'),
'area' => [
'id' => 'http_status_code',
],
];
$data['views']['combine'] = [
'title' => $this->t('Combine fields filter'),
'help' => $this->t('Combine multiple fields together and search by them.'),
'filter' => [
'id' => 'combine',
],
];
$data['views']['dropbutton'] = [
'title' => $this->t('Dropbutton'),
'help' => $this->t('Display fields in a dropbutton.'),
'field' => [
'id' => 'dropbutton',
],
];
$data['views']['display_link'] = [
'title' => $this->t('Link to display'),
'help' => $this->t('Displays a link to a path-based display of this view while keeping the filter criteria, sort criteria, pager settings and contextual filters.'),
'area' => [
'id' => 'display_link',
],
];
// Registers an entity area handler per entity type.
foreach (\Drupal::entityTypeManager()->getDefinitions() as $entity_type_id => $entity_type) {
// Excludes entity types, which cannot be rendered.
if ($entity_type->hasViewBuilderClass()) {
$label = $entity_type->getLabel();
$data['views']['entity_' . $entity_type_id] = [
'title' => $this->t('Rendered entity - @label', [
'@label' => $label,
]),
'help' => $this->t('Displays a rendered @label entity in an area.', [
'@label' => $label,
]),
'area' => [
'entity_type' => $entity_type_id,
'id' => 'entity',
],
];
}
}
// Registers an action bulk form per entity.
$all_actions = \Drupal::entityTypeManager()->getStorage('action')->loadMultiple();
foreach (\Drupal::entityTypeManager()->getDefinitions() as $entity_type => $entity_info) {
$actions = array_filter($all_actions, function (ActionConfigEntityInterface $action) use ($entity_type) {
return $action->getType() == $entity_type;
});
if (empty($actions)) {
continue;
}
$data[$entity_info->getBaseTable()][$entity_type . '_bulk_form'] = [
'title' => $this->t('Bulk update'),
'help' => $this->t('Allows users to apply an action to one or more items.'),
'field' => [
'id' => 'bulk_form',
],
];
}
// Registers views data for the entity itself.
foreach (\Drupal::entityTypeManager()->getDefinitions() as $entity_type_id => $entity_type) {
if ($entity_type->hasHandlerClass('views_data')) {
/** @var \Drupal\views\EntityViewsDataInterface $views_data */
$views_data = \Drupal::entityTypeManager()->getHandler($entity_type_id, 'views_data');
$data = NestedArray::mergeDeep($data, $views_data->getViewsData());
}
}
// Field modules can implement hook_field_views_data() to override the
// default behavior for adding fields.
$module_handler = \Drupal::moduleHandler();
$entity_type_manager = \Drupal::entityTypeManager();
if ($entity_type_manager->hasDefinition('field_storage_config')) {
/** @var \Drupal\field\FieldStorageConfigInterface $field_storage */
foreach ($entity_type_manager->getStorage('field_storage_config')->loadMultiple() as $field_storage) {
if (\Drupal::service('views.field_data_provider')->getSqlStorageForField($field_storage)) {
$provider = $field_storage->getTypeProvider();
$result = (array) $module_handler->invoke($provider === 'core' ? 'views' : $provider, 'field_views_data', [$field_storage]);
if (empty($result)) {
$result = \Drupal::service('views.field_data_provider')->defaultFieldImplementation($field_storage);
}
$module_handler->alter('field_views_data', $result, $field_storage);
if (is_array($result)) {
$data = NestedArray::mergeDeep($result, $data);
}
\Drupal::moduleHandler()->invoke($field_storage->getTypeProvider(), 'field_views_data_views_data_alter', [&$data, $field_storage]);
}
}
}
return $data;
}
/**
* Implements hook_field_views_data().
*
* The function implements the hook on behalf of 'core' because it adds a
* relationship and a reverse relationship to entity_reference field type,
* which is provided by core. This function also provides an argument plugin
* for entity_reference fields that handles title token replacement.
*/
#[Hook('field_views_data')]
public function fieldViewsData(FieldStorageConfigInterface $field_storage): array {
$data = \Drupal::service('views.field_data_provider')->defaultFieldImplementation($field_storage);
// The code below only deals with the Entity reference field type.
if ($field_storage->getType() != 'entity_reference') {
return $data;
}
$entity_type_manager = \Drupal::entityTypeManager();
$entity_type_id = $field_storage->getTargetEntityTypeId();
/** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */
$table_mapping = $entity_type_manager->getStorage($entity_type_id)->getTableMapping();
foreach ($data as $table_name => $table_data) {
// Add a relationship to the target entity type.
$target_entity_type_id = $field_storage->getSetting('target_type');
$target_entity_type = $entity_type_manager->getDefinition($target_entity_type_id);
$entity_type_id = $field_storage->getTargetEntityTypeId();
$entity_type = $entity_type_manager->getDefinition($entity_type_id);
$target_base_table = $target_entity_type->getDataTable() ?: $target_entity_type->getBaseTable();
$field_name = $field_storage->getName();
if ($target_entity_type instanceof ContentEntityTypeInterface) {
// Provide a relationship for the entity type with the entity reference
// field.
$args = ['@label' => $target_entity_type->getLabel(), '@field_name' => $field_name];
$data[$table_name][$field_name]['relationship'] = [
'title' => $this->t('@label referenced from @field_name', $args),
'label' => $this->t('@field_name: @label', $args),
'group' => $entity_type->getLabel(),
'help' => $this->t('Appears in: @bundles.', [
'@bundles' => implode(', ', $field_storage->getBundles()),
]),
'id' => 'standard',
'base' => $target_base_table,
'entity type' => $target_entity_type_id,
'base field' => $target_entity_type->getKey('id'),
'relationship field' => $field_name . '_target_id',
];
// Provide a reverse relationship for the entity type that is referenced
// by the field.
$args['@entity'] = $entity_type->getLabel();
$args['@label'] = $target_entity_type->getSingularLabel();
$pseudo_field_name = 'reverse__' . $entity_type_id . '__' . $field_name;
$data[$target_base_table][$pseudo_field_name]['relationship'] = [
'title' => $this->t('@entity using @field_name', $args),
'label' => $this->t('@field_name', [
'@field_name' => $field_name,
]),
'group' => $target_entity_type->getLabel(),
'help' => $this->t('Relate each @entity with a @field_name set to the @label.', $args),
'id' => 'entity_reverse',
'base' => $entity_type->getDataTable() ?: $entity_type->getBaseTable(),
'entity_type' => $entity_type_id,
'base field' => $entity_type->getKey('id'),
'field_name' => $field_name,
'field table' => $table_mapping->getDedicatedDataTableName($field_storage),
'field field' => $field_name . '_target_id',
'join_extra' => [
[
'field' => 'deleted',
'value' => 0,
'numeric' => TRUE,
],
],
];
}
// Provide an argument plugin that has a meaningful titleQuery()
// implementation getting the entity label.
$data[$table_name][$field_name . '_target_id']['argument']['id'] = 'entity_target_id';
$data[$table_name][$field_name . '_target_id']['argument']['target_entity_type_id'] = $target_entity_type_id;
}
return $data;
}
}

View File

@ -0,0 +1,365 @@
<?php
namespace Drupal\views;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\views\Plugin\views\HandlerBase;
use Drupal\views\Plugin\views\ViewsHandlerInterface;
/**
* This many to one helper object is used on both arguments and filters.
*
* @todo This requires extensive documentation on how this class is to
* be used. For now, look at the arguments and filters that use it. Lots
* of stuff is just pass-through but there are definitely some interesting
* areas where they interact.
*
* Any handler that uses this can have the following possibly additional
* definition terms:
* - numeric: If true, treat this field as numeric, using %d instead of %s in
* queries.
*/
class ManyToOneHelper {
use StringTranslationTrait;
/**
* Should the field use formula or alias.
*
* @var bool
*
* @see \Drupal\views\Plugin\views\argument\StringArgument::query()
*/
public bool $formula = FALSE;
/**
* The handler.
*/
public ViewsHandlerInterface $handler;
public function __construct($handler) {
$this->handler = $handler;
}
public static function defineOptions(&$options) {
$options['reduce_duplicates'] = ['default' => FALSE];
}
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
$form['reduce_duplicates'] = [
'#type' => 'checkbox',
'#title' => $this->t('Reduce duplicates'),
'#description' => $this->t("This filter can cause items that have more than one of the selected options to appear as duplicate results. If this filter causes duplicate results to occur, this checkbox can reduce those duplicates; however, the more terms it has to search for, the less performant the query will be, so use this with caution. Shouldn't be set on single-value fields, as it may cause values to disappear from display, if used on an incompatible field."),
'#default_value' => !empty($this->handler->options['reduce_duplicates']),
'#weight' => 4,
];
}
/**
* Get the field via formula or build it using alias and field name.
*
* Sometimes the handler might want us to use some kind of formula, so give it
* that option. If it wants us to do this, it must set $helper->formula = TRUE
* and implement handler->getFormula().
*/
public function getField() {
if (!empty($this->formula)) {
return $this->handler->getFormula();
}
else {
return $this->handler->tableAlias . '.' . $this->handler->realField;
}
}
/**
* Add a table to the query.
*
* This is an advanced concept; not only does it add a new instance of the
* table, but it follows the relationship path all the way down to the
* relationship link point and adds *that* as a new relationship and then adds
* the table to the relationship, if necessary.
*/
public function addTable($join = NULL, $alias = NULL) {
// This is used for lookups in the many_to_one table.
$field = $this->handler->relationship . '_' . $this->handler->table . '.' . $this->handler->field;
if (empty($join)) {
$join = $this->getJoin();
}
// See if there's a chain between us and the base relationship. If so, we
// need to create a new relationship to use.
$relationship = $this->handler->relationship;
// Determine the primary table to seek
if (empty($this->handler->query->relationships[$relationship])) {
$base_table = $this->handler->view->storage->get('base_table');
}
else {
$base_table = $this->handler->query->relationships[$relationship]['base'];
}
// Cycle through the joins. This isn't as error-safe as the normal
// ensurePath logic. Perhaps it should be.
$r_join = clone $join;
while ($r_join->leftTable != $base_table) {
$r_join = HandlerBase::getTableJoin($r_join->leftTable, $base_table);
}
// If we found that there are tables in between, add the relationship.
if ($r_join->table != $join->table) {
$relationship = $this->handler->query->addRelationship($this->handler->table . '_' . $r_join->table, $r_join, $r_join->table, $this->handler->relationship);
}
// And now add our table, using the new relationship if one was used.
$alias = $this->handler->query->addTable($this->handler->table, $relationship, $join, $alias);
// Store what values are used by this table chain so that other chains can
// automatically discard those values.
if (empty($this->handler->view->many_to_one_tables[$field])) {
$this->handler->view->many_to_one_tables[$field] = $this->handler->value;
}
else {
$this->handler->view->many_to_one_tables[$field] = array_merge($this->handler->view->many_to_one_tables[$field], $this->handler->value);
}
return $alias;
}
public function getJoin() {
return $this->handler->getJoin();
}
/**
* Provides the proper join for summary queries.
*
* This is important in part because it will cooperate with other arguments if
* possible.
*/
public function summaryJoin() {
$field = $this->handler->relationship . '_' . $this->handler->table . '.' . $this->handler->field;
$join = $this->getJoin();
// Shortcuts
$options = $this->handler->options;
$view = $this->handler->view;
$query = $this->handler->query;
if (!empty($options['require_value'])) {
$join->type = 'INNER';
}
if (empty($options['add_table']) || empty($view->many_to_one_tables[$field])) {
return $query->ensureTable($this->handler->table, $this->handler->relationship, $join);
}
else {
if (!empty($view->many_to_one_tables[$field])) {
foreach ($view->many_to_one_tables[$field] as $value) {
$join->extra = [
[
'field' => $this->handler->realField,
'operator' => '!=',
'value' => $value,
'numeric' => !empty($this->handler->definition['numeric']),
],
];
}
}
return $this->addTable($join);
}
}
/**
* Override ensureMyTable so we can control how this joins in.
*
* The operator actually has influence over joining.
*/
public function ensureMyTable() {
if (!isset($this->handler->tableAlias)) {
// Case 1: Operator is an 'or' and we're not reducing duplicates.
// We hence get the absolute simplest:
$field = $this->handler->relationship . '_' . $this->handler->table . '.' . $this->handler->field;
if ($this->handler->operator == 'or' && empty($this->handler->options['reduce_duplicates'])) {
if (empty($this->handler->options['add_table']) && empty($this->handler->view->many_to_one_tables[$field])) {
// Query optimization, INNER joins are slightly faster, so use them
// when we know we can.
$join = $this->getJoin();
$group = $this->handler->options['group'] ?? FALSE;
// Only if there is no group with OR operator.
if (isset($join) && !($group && $this->handler->query->where[$group]['type'] === 'OR')) {
$join->type = 'INNER';
}
$this->handler->tableAlias = $this->handler->query->ensureTable($this->handler->table, $this->handler->relationship, $join);
$this->handler->view->many_to_one_tables[$field] = $this->handler->value;
}
else {
$join = $this->getJoin();
$join->type = 'LEFT';
if (!empty($this->handler->view->many_to_one_tables[$field])) {
foreach ($this->handler->view->many_to_one_tables[$field] as $value) {
$join->extra = [
[
'field' => $this->handler->realField,
'operator' => '!=',
'value' => $value,
'numeric' => !empty($this->handler->definition['numeric']),
],
];
}
}
$this->handler->tableAlias = $this->addTable($join);
}
return $this->handler->tableAlias;
}
// Case 2: it's an 'and' or an 'or'.
// We do one join per selected value.
if ($this->handler->operator != 'not') {
// Clone the join for each table:
$this->handler->tableAliases = [];
foreach ($this->handler->value as $value) {
$join = $this->getJoin();
if ($this->handler->operator == 'and') {
$join->type = 'INNER';
}
$join->extra = [
[
'field' => $this->handler->realField,
'value' => $value,
'numeric' => !empty($this->handler->definition['numeric']),
],
];
// The table alias needs to be unique to this value across the
// multiple times the filter or argument is called by the view.
if (!isset($this->handler->view->many_to_one_aliases[$field][$value])) {
if (!isset($this->handler->view->many_to_one_count[$this->handler->table])) {
$this->handler->view->many_to_one_count[$this->handler->table] = 0;
}
$this->handler->view->many_to_one_aliases[$field][$value] = $this->handler->table . '_value_' . ($this->handler->view->many_to_one_count[$this->handler->table]++);
}
$this->handler->tableAliases[$value] = $this->addTable($join, $this->handler->view->many_to_one_aliases[$field][$value]);
// Set tableAlias to the first of these.
if (empty($this->handler->tableAlias)) {
$this->handler->tableAlias = $this->handler->tableAliases[$value];
}
}
}
// Case 3: it's a 'not'.
// We just do one join. We'll add a where clause during
// the query phase to ensure that $table.$field IS NULL.
else {
$join = $this->getJoin();
$join->type = 'LEFT';
$join->extra = [];
$join->extraOperator = 'OR';
foreach ($this->handler->value as $value) {
$join->extra[] = [
'field' => $this->handler->realField,
'value' => $value,
'numeric' => !empty($this->handler->definition['numeric']),
];
}
$this->handler->tableAlias = $this->addTable($join);
}
}
return $this->handler->tableAlias;
}
/**
* Provides a unique placeholders for handlers.
*/
protected function placeholder() {
return $this->handler->query->placeholder($this->handler->options['table'] . '_' . $this->handler->options['field']);
}
public function addFilter() {
if (empty($this->handler->value)) {
return;
}
$this->handler->ensureMyTable();
// Shorten some variables:
$field = $this->getField();
$options = $this->handler->options;
$operator = $this->handler->operator;
$formula = !empty($this->formula);
$value = $this->handler->value;
if (empty($options['group'])) {
$options['group'] = 0;
}
// If $add_condition is set to FALSE, a single expression is enough. If it
// is set to TRUE, conditions will be added.
$add_condition = TRUE;
if ($operator == 'not') {
$value = NULL;
$operator = 'IS NULL';
$add_condition = FALSE;
}
elseif ($operator == 'or' && empty($options['reduce_duplicates'])) {
if (count($value) > 1) {
$operator = 'IN';
}
else {
$value = is_array($value) ? array_pop($value) : $value;
$operator = '=';
}
$add_condition = FALSE;
}
if (!$add_condition) {
if ($formula) {
$placeholder = $this->placeholder();
if ($operator == 'IN') {
$operator = "$operator IN($placeholder)";
}
else {
$operator = "$operator $placeholder";
}
$placeholders = [
$placeholder => $value,
];
$this->handler->query->addWhereExpression($options['group'], "$field $operator", $placeholders);
}
else {
$placeholder = $this->placeholder();
if (count($this->handler->value) > 1) {
$placeholder .= '[]';
if ($operator == 'IS NULL') {
$this->handler->query->addWhereExpression($options['group'], "$field $operator");
}
else {
$this->handler->query->addWhereExpression($options['group'], "$field $operator($placeholder)", [$placeholder => $value]);
}
}
else {
if ($operator == 'IS NULL') {
$this->handler->query->addWhereExpression($options['group'], "$field $operator");
}
else {
$this->handler->query->addWhereExpression($options['group'], "$field $operator $placeholder", [$placeholder => $value]);
}
}
}
}
if ($add_condition) {
$field = $this->handler->realField;
$clause = $operator == 'or' ? $this->handler->query->getConnection()->condition('OR') : $this->handler->query->getConnection()->condition('AND');
foreach ($this->handler->tableAliases as $value => $alias) {
$clause->condition("$alias.$field", $value);
}
// Implode on either AND or OR.
$this->handler->query->addWhere($options['group'], $clause);
}
}
}

View File

@ -0,0 +1,161 @@
<?php
namespace Drupal\views\Plugin\Block;
use Drupal\Component\Utility\Xss;
use Drupal\Core\Block\Attribute\Block;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\views\Element\View;
use Drupal\views\Plugin\Derivative\ViewsBlock as ViewsBlockDeriver;
use Drupal\Core\Entity\EntityInterface;
/**
* Provides a generic Views block.
*/
#[Block(
id: "views_block",
admin_label: new TranslatableMarkup("Views Block"),
deriver: ViewsBlockDeriver::class
)]
class ViewsBlock extends ViewsBlockBase {
/**
* {@inheritdoc}
*/
public function build() {
// If the block plugin is invalid, there is nothing to do.
if (!method_exists($this->view->display_handler, 'preBlockBuild')) {
return [];
}
$this->view->display_handler->preBlockBuild($this);
$args = [];
foreach ($this->view->display_handler->getHandlers('argument') as $argument_name => $argument) {
// Initialize the argument value. Work around a limitation in
// \Drupal\views\ViewExecutable::_buildArguments() that skips processing
// later arguments if an argument with default action "ignore" and no
// argument is provided.
$args[$argument_name] = $argument->options['default_action'] == 'ignore' ? 'all' : NULL;
if (!empty($this->context[$argument_name])) {
if ($value = $this->context[$argument_name]->getContextValue()) {
// Context values are often entities, but views arguments expect to
// receive just the entity ID, convert it.
if ($value instanceof EntityInterface) {
$value = $value->id();
}
$args[$argument_name] = $value;
}
}
}
// We ask ViewExecutable::buildRenderable() to avoid creating a render cache
// entry for the view output by passing FALSE, because we're going to cache
// the whole block instead.
if ($output = $this->view->buildRenderable($this->displayID, array_values($args), FALSE)) {
// Before returning the block output, convert it to a renderable array
// with contextual links.
$this->addContextualLinks($output);
// Block module expects to get a final render array, without another
// top-level #pre_render callback. So, here we make sure that Views'
// #pre_render callback has already been applied.
$output = View::preRenderViewElement($output);
// Inject the overridden block title into the view.
if (!empty($this->configuration['views_label'])) {
$this->view->setTitle($this->configuration['views_label']);
}
// Override the block title to match the view title.
if ($this->view->getTitle()) {
$output['#title'] = ['#markup' => $this->view->getTitle(), '#allowed_tags' => Xss::getHtmlTagList()];
}
// When view_build is empty, the actual render array output for this View
// is going to be empty. In that case, return just #cache, so that the
// render system knows the reasons (cache contexts & tags) why this Views
// block is empty, and can cache it accordingly.
if (empty($output['view_build'])) {
$output = ['#cache' => $output['#cache']];
}
return $output;
}
return [];
}
/**
* {@inheritdoc}
*/
public function getConfiguration() {
$configuration = parent::getConfiguration();
// Set the label to the static title configured in the view.
if (!empty($configuration['views_label'])) {
$configuration['label'] = $configuration['views_label'];
}
return $configuration;
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
$settings = parent::defaultConfiguration();
if ($this->displaySet) {
$settings += $this->view->display_handler->blockSettings($settings);
}
// Set custom cache settings.
if (isset($this->pluginDefinition['cache'])) {
$settings['cache'] = $this->pluginDefinition['cache'];
}
return $settings;
}
/**
* {@inheritdoc}
*/
public function blockForm($form, FormStateInterface $form_state) {
if ($this->displaySet) {
return $this->view->display_handler->blockForm($this, $form, $form_state);
}
return [];
}
/**
* {@inheritdoc}
*/
public function blockValidate($form, FormStateInterface $form_state) {
if ($this->displaySet) {
$this->view->display_handler->blockValidate($this, $form, $form_state);
}
}
/**
* {@inheritdoc}
*/
public function blockSubmit($form, FormStateInterface $form_state) {
parent::blockSubmit($form, $form_state);
if ($this->displaySet) {
$this->view->display_handler->blockSubmit($this, $form, $form_state);
}
}
/**
* {@inheritdoc}
*/
public function getMachineNameSuggestion() {
$this->view->setDisplay($this->displayID);
return 'views_block__' . $this->view->storage->id() . '_' . $this->view->current_display;
}
}

View File

@ -0,0 +1,268 @@
<?php
namespace Drupal\views\Plugin\Block;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Url;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\views\ViewExecutableFactory;
use Drupal\Core\Entity\EntityStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Session\AccountInterface;
/**
* Base class for Views block plugins.
*/
abstract class ViewsBlockBase extends BlockBase implements ContainerFactoryPluginInterface {
/**
* The View executable object.
*
* @var \Drupal\views\ViewExecutable
*/
protected $view;
/**
* The display ID being used for this View.
*
* @var string
*/
protected $displayID;
/**
* Indicates whether the display was successfully set.
*
* @var bool
*/
protected $displaySet;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $user;
/**
* Constructs a \Drupal\views\Plugin\Block\ViewsBlockBase object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin ID for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\views\ViewExecutableFactory $executable_factory
* The view executable factory.
* @param \Drupal\Core\Entity\EntityStorageInterface $storage
* The views storage.
* @param \Drupal\Core\Session\AccountInterface $user
* The current user.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, ViewExecutableFactory $executable_factory, EntityStorageInterface $storage, AccountInterface $user) {
$this->pluginId = $plugin_id;
$delta = $this->getDerivativeId();
[$name, $this->displayID] = explode('-', $delta, 2);
// Load the view.
$view = $storage->load($name);
$this->view = $executable_factory->get($view);
$this->displaySet = $this->view->setDisplay($this->displayID);
$this->user = $user;
parent::__construct($configuration, $plugin_id, $plugin_definition);
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration, $plugin_id, $plugin_definition,
$container->get('views.executable'),
$container->get('entity_type.manager')->getStorage('view'),
$container->get('current_user')
);
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
$contexts = $this->view->display_handler->getCacheMetadata()->getCacheContexts();
return Cache::mergeContexts(parent::getCacheContexts(), $contexts);
}
/**
* {@inheritdoc}
*/
public function getCacheTags() {
$tags = $this->view->display_handler->getCacheMetadata()->getCacheTags();
return Cache::mergeTags(parent::getCacheTags(), $tags);
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
$max_age = $this->view->display_handler->getCacheMetadata()->getCacheMaxAge();
return Cache::mergeMaxAges(parent::getCacheMaxAge(), $max_age);
}
/**
* {@inheritdoc}
*/
protected function blockAccess(AccountInterface $account) {
if ($this->view->access($this->displayID)) {
$access = AccessResult::allowed();
}
else {
$access = AccessResult::forbidden();
}
return $access;
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return ['views_label' => ''];
}
/**
* {@inheritdoc}
*/
public function getPreviewFallbackString() {
if (!empty($this->pluginDefinition["admin_label"])) {
return $this->t('"@view" views block', ['@view' => $this->pluginDefinition["admin_label"]]);
}
else {
return $this->t('"@view" views block', ['@view' => $this->view->storage->label() . '::' . $this->displayID]);
}
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form = parent::buildConfigurationForm($form, $form_state);
// Set the default label to '' so the views internal title is used.
$form['label']['#default_value'] = '';
$form['label']['#access'] = FALSE;
// Unset the machine_name provided by BlockForm.
unset($form['id']['#machine_name']['source']);
// Prevent users from changing the auto-generated block machine_name.
$form['id']['#access'] = FALSE;
$form['#pre_render'][] = '\Drupal\views\Plugin\views\PluginBase::preRenderAddFieldsetMarkup';
// Allow to override the label on the actual page.
$form['views_label_checkbox'] = [
'#type' => 'checkbox',
'#title' => $this->t('Override title'),
'#default_value' => !empty($this->configuration['views_label']),
];
$form['views_label_fieldset'] = [
'#type' => 'fieldset',
'#states' => [
'visible' => [
[
':input[name="settings[views_label_checkbox]"]' => ['checked' => TRUE],
],
],
],
];
$form['views_label'] = [
'#title' => $this->t('Title'),
'#type' => 'textfield',
'#default_value' => $this->configuration['views_label'] ?: $this->view->getTitle(),
'#states' => [
'visible' => [
[
':input[name="settings[views_label_checkbox]"]' => ['checked' => TRUE],
],
],
],
'#fieldset' => 'views_label_fieldset',
];
if ($this->view->storage->access('edit') && \Drupal::moduleHandler()->moduleExists('views_ui')) {
$form['views_label']['#description'] = $this->t('Changing the title here means it cannot be dynamically altered anymore. (Try changing it directly in <a href=":url">@name</a>.)', [':url' => Url::fromRoute('entity.view.edit_display_form', ['view' => $this->view->storage->id(), 'display_id' => $this->displayID])->toString(), '@name' => $this->view->storage->label()]);
}
else {
$form['views_label']['#description'] = $this->t('Changing the title here means it cannot be dynamically altered anymore.');
}
return $form;
}
/**
* {@inheritdoc}
*/
public function blockSubmit($form, FormStateInterface $form_state) {
if (!$form_state->isValueEmpty('views_label_checkbox')) {
$this->configuration['views_label'] = $form_state->getValue('views_label');
}
else {
$this->configuration['views_label'] = '';
}
$form_state->unsetValue('views_label_checkbox');
}
/**
* Converts Views block content to a renderable array with contextual links.
*
* @param string|array $output
* A string|array representing the block. This will be modified to be a
* renderable array, containing the optional '#contextual_links' property
* (if there are any contextual links associated with the block).
* @param string $block_type
* The type of the block. If it's 'block' it's a regular views display,
* but 'exposed_filter' exist as well.
*/
protected function addContextualLinks(&$output, $block_type = 'block') {
// Do not add contextual links to an empty block.
if (!empty($output)) {
// Contextual links only work on blocks whose content is a renderable
// array, so if the block contains a string of already-rendered markup,
// convert it to an array.
if (is_string($output)) {
$output = ['#markup' => $output];
}
// views_add_contextual_links() needs the following information in
// order to be attached to the view.
$output['#view_id'] = $this->view->storage->id();
$output['#view_display_show_admin_links'] = $this->view->getShowAdminLinks();
$output['#view_display_plugin_id'] = $this->view->display_handler->getPluginId();
views_add_contextual_links($output, $block_type, $this->displayID);
}
}
/**
* Gets the view executable.
*
* @return \Drupal\views\ViewExecutable
* The view executable.
*
* @todo revisit after https://www.drupal.org/node/3027653. This method was
* added in https://www.drupal.org/node/3002608, but should not be
* necessary once block plugins can determine if they are being previewed.
*/
public function getViewExecutable() {
return $this->view;
}
/**
* {@inheritdoc}
*/
public function createPlaceholder(): bool {
return TRUE;
}
}

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