Skip to content
Snippets Groups Projects
Commit aacafade authored by Roderik Muit's avatar Roderik Muit
Browse files

Issue #3455435 by roderik: Change configuration structure for CE display 'components'

parent 6ff39907
No related branches found
No related tags found
1 merge request!73Resolve #3455435 "Change configuration structure"
Pipeline #221981 passed with warnings
Showing
with 484 additions and 394 deletions
......@@ -38,9 +38,9 @@ custom_elements.entity_ce_display.*.*.*:
formatter:
type: string
label: 'Custom element formatter ID'
name:
field_name:
type: string
label: 'Custom element attribute/slot name'
label: 'Name of the entity field'
is_slot:
type: bool
label: 'is Slot'
......@@ -53,12 +53,6 @@ custom_elements.entity_ce_display.*.*.*:
configuration:
type: custom_elements.field_formatter.configuration.[%parent.formatter]
label: 'Settings'
hidden:
type: sequence
label: 'Hidden'
sequence:
type: boolean
label: 'Value'
# Default schema for entity display field with undefined type.
custom_elements.field_formatter.configuration.*:
......
uuid: 2f308c3f-de54-4727-8ff0-34ce286446ab
langcode: en-gb
langcode: en
status: true
dependencies:
config:
......@@ -12,29 +11,10 @@ mode: default
forceAutoProcessing: false
customElementName: media-gallery-default
content:
field_media_images:
sources:
formatter: entity_ce_render
name: sources
field_name: field_media_images
is_slot: false
weight: 0
region: content
hidden:
bundle: true
changed: true
created: true
default_langcode: true
langcode: true
metatag: true
mid: true
name: true
path: true
revision_created: true
revision_default: true
revision_log_message: true
revision_translation_affected: true
revision_user: true
status: true
thumbnail: true
uid: true
uuid: true
vid: true
hidden: null
uuid: dadd4eca-7032-44fa-9d3a-18f605a44231
langcode: en-gb
langcode: en
status: true
dependencies:
config:
......@@ -16,48 +15,28 @@ mode: default
forceAutoProcessing: false
customElementName: media-image-default
content:
field_copyright:
caption:
formatter: flattened
name: copyright
field_name: field_description
is_slot: false
weight: 1
region: content
copyright:
formatter: flattened
field_name: field_copyright
is_slot: false
weight: 2
region: content
field_description:
source:
formatter: flattened
name: caption
field_name: field_source
is_slot: false
weight: 1
weight: 3
region: content
field_image:
src:
formatter: 'field:image_url'
name: src
field_name: field_image
is_slot: false
weight: 0
region: content
field_source:
formatter: flattened
name: source
is_slot: false
weight: 3
region: content
hidden:
bundle: true
changed: true
created: true
default_langcode: true
field_tags: true
langcode: true
metatag: true
mid: true
name: true
path: true
revision_created: true
revision_default: true
revision_log_message: true
revision_translation_affected: true
revision_user: true
status: true
thumbnail: true
uid: true
uuid: true
vid: true
hidden: null
uuid: 6126ab8b-b1ca-4ae7-90cf-4b7223af5ece
langcode: en-gb
langcode: en
status: true
dependencies:
config:
......@@ -17,53 +16,34 @@ mode: full
forceAutoProcessing: false
customElementName: media-image-full
content:
field_copyright:
copyright:
formatter: flattened
name: copyright
field_name: field_copyright
is_slot: false
weight: 3
region: content
field_description:
description:
formatter: 'field:text_default'
name: description
field_name: field_description
is_slot: false
weight: 2
region: content
field_image:
image:
formatter: file
name: image
field_name: field_image
is_slot: false
weight: 0
region: content
field_source:
source:
formatter: flattened
name: source
field_name: field_source
is_slot: false
weight: 4
region: content
thumbnail:
formatter: file
name: thumbnail
field_name: thumbnail
is_slot: false
weight: 1
region: content
hidden:
bundle: true
changed: true
created: true
default_langcode: true
field_tags: true
langcode: true
metatag: true
mid: true
name: true
path: true
revision_created: true
revision_default: true
revision_log_message: true
revision_translation_affected: true
revision_user: true
status: true
uid: true
uuid: true
vid: true
hidden: null
uuid: aeb9c081-d23c-4b41-9e3a-0847fe3886d3
langcode: en-gb
langcode: en
status: true
dependencies:
config:
......@@ -14,31 +13,10 @@ mode: default
forceAutoProcessing: false
customElementName: media-twitter-default
content:
field_url:
src:
formatter: flattened
name: src
field_name: field_url
is_slot: false
weight: 0
region: content
hidden:
bundle: true
changed: true
created: true
default_langcode: true
field_author: true
field_content: true
langcode: true
metatag: true
mid: true
name: true
path: true
revision_created: true
revision_default: true
revision_log_message: true
revision_translation_affected: true
revision_user: true
status: true
thumbnail: true
uid: true
uuid: true
vid: true
hidden: null
uuid: 0e6f8e92-1fcf-43c7-a9f2-719c0ac6f56f
langcode: en-gb
langcode: en
status: true
dependencies:
config:
......@@ -16,35 +15,12 @@ mode: default
forceAutoProcessing: false
customElementName: media-video-default
content:
field_media_video_embed_field:
src:
formatter: video_embed
name: src
field_name: field_media_video_embed_field
is_slot: false
weight: 0
region: content
configuration:
thumbnail_force_https: '1'
hidden:
bundle: true
changed: true
created: true
default_langcode: true
field_caption: true
field_copyright: true
field_description: true
field_source: true
langcode: true
metatag: true
mid: true
name: true
path: true
revision_created: true
revision_default: true
revision_log_message: true
revision_translation_affected: true
revision_user: true
status: true
thumbnail: true
uid: true
uuid: true
vid: true
hidden: null
uuid: fdff2c67-c117-42bc-ab6a-51a4c4e91ad1
langcode: en-gb
langcode: en
status: true
dependencies:
config:
......@@ -12,27 +11,13 @@ mode: default
forceAutoProcessing: false
customElementName: pg-gallery
content:
field_media:
media:
formatter: entity_ce_render
name: media
field_name: field_media
is_slot: false
weight: 0
region: content
configuration:
mode: full
flatten: '1'
hidden:
behavior_settings: true
created: true
default_langcode: true
id: true
langcode: true
parent_field_name: true
parent_id: true
parent_type: true
revision_default: true
revision_id: true
revision_translation_affected: true
status: true
type: true
uuid: true
hidden: null
uuid: 30ca9c5d-264e-4e70-b0e5-f80964373fb4
langcode: en-gb
langcode: en
status: true
dependencies:
config:
......@@ -12,27 +11,13 @@ mode: default
forceAutoProcessing: false
customElementName: pg-image
content:
field_image:
image:
formatter: entity_ce_render
name: image
field_name: field_image
is_slot: false
weight: 0
region: content
configuration:
mode: default
flatten: '1'
hidden:
behavior_settings: true
created: true
default_langcode: true
id: true
langcode: true
parent_field_name: true
parent_id: true
parent_type: true
revision_default: true
revision_id: true
revision_translation_affected: true
status: true
type: true
uuid: true
hidden: null
uuid: 3013ac4f-9ae2-4cb5-88bf-c1c9646af1bb
langcode: en-gb
langcode: en
status: true
dependencies:
config:
......@@ -12,24 +11,10 @@ mode: default
forceAutoProcessing: false
customElementName: pg-link
content:
field_link:
link:
formatter: flattened
name: link
field_name: field_link
is_slot: false
weight: 0
region: content
hidden:
behavior_settings: true
created: true
default_langcode: true
id: true
langcode: true
parent_field_name: true
parent_id: true
parent_type: true
revision_default: true
revision_id: true
revision_translation_affected: true
status: true
type: true
uuid: true
hidden: null
uuid: 33dfe954-07d8-46ac-8524-181ed9c9c5c3
langcode: en-gb
langcode: en
status: true
dependencies:
config:
......@@ -12,24 +11,10 @@ mode: default
forceAutoProcessing: false
customElementName: pg-quote
content:
field_text:
default:
formatter: auto
name: default
field_name: field_text
is_slot: true
weight: 1
region: content
hidden:
behavior_settings: true
created: true
default_langcode: true
id: true
langcode: true
parent_field_name: true
parent_id: true
parent_type: true
revision_default: true
revision_id: true
revision_translation_affected: true
status: true
type: true
uuid: true
hidden: null
uuid: 134d7b39-edfd-483d-bd9f-83828bba86a2
langcode: en-gb
langcode: en
status: true
dependencies:
config:
......@@ -10,32 +9,19 @@ id: paragraph.text.default
targetEntityType: paragraph
bundle: text
mode: default
forceAutoProcessing: false
customElementName: pg-text
content:
field_text:
default:
formatter: auto
name: default
field_name: field_text
is_slot: true
weight: 1
region: content
field_title:
title:
formatter: auto
name: title
field_name: field_title
is_slot: false
weight: 0
region: content
hidden:
behavior_settings: true
created: true
default_langcode: true
id: true
langcode: true
parent_field_name: true
parent_id: true
parent_type: true
revision_default: true
revision_id: true
revision_translation_affected: true
status: true
type: true
uuid: true
hidden: null
uuid: 11a1b2cf-2ad5-47aa-9559-7fd0cc064838
langcode: en-gb
langcode: en
status: true
dependencies:
config:
......@@ -12,27 +11,13 @@ mode: default
forceAutoProcessing: false
customElementName: pg-twitter
content:
field_media:
media:
formatter: entity_ce_render
name: media
field_name: field_media
is_slot: false
weight: 0
region: content
configuration:
mode: full
flatten: '1'
hidden:
behavior_settings: true
created: true
default_langcode: true
id: true
langcode: true
parent_field_name: true
parent_id: true
parent_type: true
revision_default: true
revision_id: true
revision_translation_affected: true
status: true
type: true
uuid: true
hidden: null
uuid: 713b6f2d-d57e-4d19-8acb-377c96af2419
langcode: en-gb
langcode: en
status: true
dependencies:
config:
......@@ -12,27 +11,13 @@ mode: default
forceAutoProcessing: false
customElementName: pg-video
content:
field_video:
video:
formatter: entity_ce_render
name: video
field_name: field_video
is_slot: false
weight: 0
region: content
configuration:
mode: full
flatten: '1'
hidden:
behavior_settings: true
created: true
default_langcode: true
id: true
langcode: true
parent_field_name: true
parent_id: true
parent_type: true
revision_default: true
revision_id: true
revision_translation_affected: true
status: true
type: true
uuid: true
hidden: null
......@@ -16,6 +16,7 @@ use Drupal\Core\Form\SubformState;
use Drupal\Core\Url;
use Drupal\custom_elements\CustomElementsFieldFormatterInterface;
use Drupal\custom_elements\CustomElementsFieldFormatterPluginManager;
use Drupal\custom_elements\Entity\EntityCeDisplayInterface;
use Drupal\field_ui\FieldUI;
use Drupal\field_ui\Form\EntityDisplayFormBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
......@@ -155,6 +156,12 @@ class EntityCustomElementsDisplayEditForm extends EntityDisplayFormBase {
// the form must still display/save the checkboxes for enabling view modes.
// @todo test: In this case, do not save the CE entity at all?
if (!$layout_builder_enabled || $entity->getMode() === 'default') {
if (!$layout_builder_enabled) {
$form['help'] = [
'#type' => 'markup',
'#markup' => $this->t("<p><strong>Warning: this user interface is subject to change and contains some bugs. As a result:</strong><ul><li><strong>If you want to rename a 'Key': immediately afterwards, press TAB and then press Save for each individual renamed key.</strong></li><li><strong>When editing anything else, do not rename keys at the same time.</strong></li></ul></p>"),
];
}
$form = parent::form($form, $form_state);
if ($layout_builder_enabled) {
......@@ -171,6 +178,13 @@ class EntityCustomElementsDisplayEditForm extends EntityDisplayFormBase {
}
}
// Remove EntityForm::afterBuild(), which calls copyFormValuesToEntity();
// we cannot update $this->entity before form values are properly validated.
// Any other validation functions must take this into account.
if (!empty($form['#after_build'])) {
$form['#after_build'] = array_diff($form['#after_build'], ['::afterBuild']);
}
return $form;
}
......@@ -225,17 +239,35 @@ class EntityCustomElementsDisplayEditForm extends EntityDisplayFormBase {
*
* @return array
* A table row array.
*
* @todo Redo this with UI revamp in #3455435. parent:form() currently loops
* through field definitions, but we likely want to loop through component
* names instead, so that multiple components for the same fields can be
* set. This means the parameter defenition of buildFieldRow() likey isn't
* good to use anymore.
* @todo At the same time (or later?), decide whether we can support other
* 'kinds of formatters' (like static values, #3446287) in that same loop.
* If not, we'll need to create some separate loop later (just like
* parent::form() loops through the extraFields at the moment). Ordering
* likely doesn't matter much, since this is going to be reordered by
* weight later, anyway.
* @todo Doublecheck whether the buildExtraFieldRow() calls in parent::form()
* do anything, and whether they can be removed.
*/
protected function buildFieldRow(FieldDefinitionInterface $field_definition, array $form, FormStateInterface $form_state) {
$display_options = NULL;
$field_name = $field_definition->getName();
$default_name = str_starts_with($field_name, 'field_') ? substr($field_name, strlen('field_')) : $field_name;
$label = $field_definition->getLabel();
$display_options = $this->entity->getComponent($field_name);
// Disable fields without any applicable plugins.
if (empty($this->getApplicablePluginOptions($field_definition))) {
$this->entity->removeComponent($field_name);
$display_options = $this->entity->getComponent($field_name);
$default_name = str_starts_with($field_name, 'field_') ? substr($field_name, strlen('field_')) : $field_name;
$component_name = $this->getComponentNameFromFieldName($field_name, NULL, FALSE);
if ($component_name) {
$display_options = $this->entity->getComponent($component_name);
// Disable fields without any applicable plugins.
if (empty($this->getApplicablePluginOptions($field_definition))) {
$this->entity->removeComponent($component_name);
$display_options = $this->entity->getComponent($component_name);
}
}
$field_row['human_name'] = [
......@@ -245,7 +277,7 @@ class EntityCustomElementsDisplayEditForm extends EntityDisplayFormBase {
'#type' => 'textfield',
'#title' => $this->t('Attribute / Slot name'),
'#title_display' => 'invisible',
'#default_value' => $display_options['name'] ?? $default_name,
'#default_value' => $component_name ?: $default_name,
'#size' => 20,
'#required' => empty($display_options) || $display_options['region'] != 'hidden',
];
......@@ -291,6 +323,10 @@ class EntityCustomElementsDisplayEditForm extends EntityDisplayFormBase {
'#default_value' => $field_name,
'#attributes' => ['class' => ['field-name']],
],
'existing_name' => [
'#type' => 'hidden',
'#default_value' => $component_name,
],
],
'region' => [
'#type' => 'select',
......@@ -303,7 +339,10 @@ class EntityCustomElementsDisplayEditForm extends EntityDisplayFormBase {
];
// Get the corresponding plugin object.
$plugin = $this->entity->getRenderer($field_name);
// @todo Safely remove the case for !$component_name in #3446485 / second
// call parameter, when all rows represent components.
$plugin = $component_name ? $this->entity->getRenderer($component_name)
: $this->entity->getRenderer($field_name, TRUE);
if ($plugin) {
$field_row = $this->buildFieldRowPluginForm($field_row, $field_name, $plugin, $form, $form_state);
......@@ -421,7 +460,71 @@ class EntityCustomElementsDisplayEditForm extends EntityDisplayFormBase {
}
/**
* Form validation callback to validate plugin configuration form.
* Gets component name from field name.
*
* This is temporary code as long as the UI still has one row per field. It
* doesn't check whether the CE display has multiple components with the
* same field name - which isn't supported by the UI yet. There is also no
* check on whether a CE display has 'non-field' components.
*
* WARNING: Re-saving such CE displays through the UI will mess up data!
*
* @return string
* The component name.
*/
private function getComponentNameFromFieldName(string $field_name, $entity = NULL, $log = TRUE): string {
if (!isset($entity)) {
$entity = $this->entity;
}
$component_name = '';
$components = $entity->getComponents();
foreach ($components as $name => $component) {
if (isset($component['field_name']) && $component['field_name'] == $field_name) {
$component_name = $name;
}
}
if (!$component_name && $log) {
$this->logger('custom_elements')->warning('No component name found for field @field_name. Alpha UI code is unstable.', ['@field_name' => $field_name]);
}
return $component_name;
}
/**
* Form validation callback for the full form.
*
* @param array $form
* A nested array of form elements comprising the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
parent::validateForm($form, $form_state);
$form_values = $form_state->getValues();
// Validate that no duplicate component name is used.
// @todo there still is a bug with this, as long as the 'name' element
// still has AJAX behavior / triggers requests while renaming individual
// duplicate elements. Fix (remove AJAX behavior) in #3446485.
$component_names_used = [];
foreach ($form['#fields'] as $field_name) {
$name = $form_values['fields'][$field_name]['name'];
if (isset($component_names_used[$name])) {
$form_state->setError($form['fields'][$field_name]['name'], $this->t('Duplicate name %name', ['%name' => $name]));
}
$component_names_used[$name] = TRUE;
}
// If the form isn't being submitted yet: need to set all components in
// the form's entity, so all plugin forms are rebuilt correctly.
if (!$form_state->getErrors() && !empty($form_state->getTriggeringElement()['#ajax'])) {
$this->doCopyFormValuesToEntity($this->entity, $form, $form_state);
}
}
/**
* Form validation callback for plugin configuration form.
*
* @param array $form
* A nested array of form elements comprising the form.
......@@ -429,44 +532,93 @@ class EntityCustomElementsDisplayEditForm extends EntityDisplayFormBase {
* The current state of the form.
*/
public function validatePluginConfigurationForm(array &$form, FormStateInterface $form_state) {
// We don't rebuild the entity before validation. The formatter cannot
// change when plugin configuration is edited anyway, so we can
// instantiate the old one. Also that means plugins get "validate" invoked
// before "submit", what makes sense.
/** @var \Drupal\custom_elements\Entity\EntityCeDisplayInterface $entity */
$entity = $this->entity;
// Allow formatters to validate their config.
// The entity was not populated yet on afterBuild(), because we cannot
// guarantee correctness of all components when they are being renamed. So,
// temporarily set current component into cloned entity to make sure we can
// at least validate its configuration form.
$validate_entity = clone $this->entity;
$field_name = $form_state->getTriggeringElement()['#field_name'];
$plugin = $entity->getRenderer($field_name);
$plugin_form =& $form['fields'][$field_name]['plugin']['settings_edit_form']['form'];
$subform_state = SubformState::createForSubform($plugin_form, $form, $form_state);
$plugin->validateConfigurationForm($plugin_form, $subform_state);
$component_name = $this->getComponentNameFromFieldName($field_name, $validate_entity);
$this->doCopyFormValuesToEntity($validate_entity, $form, $form_state, $component_name);
// Allow the 'triggering' formatter to validate its configuration.
$plugin = $validate_entity->getRenderer($component_name);
if ($plugin) {
$plugin_form =& $form['fields'][$field_name]['plugin']['settings_edit_form']['form'];
$subform_state = SubformState::createForSubform($plugin_form, $form, $form_state);
$plugin->validateConfigurationForm($plugin_form, $subform_state);
}
else {
// Should never happen. Not a user-friendly message.
$form_state->setError($form['fields'][$field_name]['name'], $this->t('Cannot find renderer for component @name', ['@name' => $component_name]));
}
}
/**
* {@inheritdoc}
*/
protected function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state) {
/** @var \Drupal\custom_elements\Entity\EntityCeDisplayInterface $entity */
$form_values = $form_state->getValues();
// See form(): this is only called on submit, with form values validated.
$this->doCopyFormValuesToEntity($entity, $form, $form_state);
}
/**
* Copies form values, or a subset, to entity properties.
*
* This should not change existing entity properties that are not being edited
* by this form.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity the current form should operate upon.
* @param array $form
* A nested array of form elements comprising the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param string $validate_component
* (Optional) Only make sure the basic properties of the specified component
* are updated, in order to be able to validate the full component's config.
* Do not copy the component's full (unvalidated) configuration yet; leave
* other component's values in an undefined state.
*/
protected function doCopyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state, string $validate_component = '') {
assert($entity instanceof EntityCeDisplayInterface);
$form_values = $form_state->getValues();
if ($this->entity instanceof EntityWithPluginCollectionInterface) {
// Do not manually update values represented by plugin collections.
$form_values = array_diff_key($form_values, $this->entity->getPluginCollections());
}
// 'Rename' components, in a way that components can switch names.
$this->renameEntityComponents($entity, $form, $form_state);
// Collect data for 'regular' fields.
// @todo Change this structure in #3446485: $form keys will be component
// names, not field names. Rename '#fields' (here and elsewhere)?
foreach ($form['#fields'] as $field_name) {
$values = $form_values['fields'][$field_name];
$component_name = $values['name'];
if ($validate_component && $validate_component !== $component_name) {
continue;
}
if ($values['region'] == 'hidden') {
$entity->removeComponent($field_name);
}
else {
$options = $entity->getComponent($field_name);
// Update field settings only if the submit handler told us to.
if ($form_state->get('plugin_settings_update') === $field_name) {
$plugin = $entity->getRenderer($field_name);
$options = $entity->getComponent($component_name);
$options['formatter'] = $values['formatter'];
$options['field_name'] = $field_name;
$options['is_slot'] = (bool) $values['is_slot'];
$options['weight'] = $values['weight'];
$options['region'] = $values['region'];
// Update field settings only if the submit handler told us to. Ignore
// if we're being called to validate these settings.
if ($form_state->get('plugin_settings_update') === $field_name && !$validate_component) {
$component_name = $this->getComponentNameFromFieldName($field_name);
// getRenderer() needs basic properties to be updated.
$entity->setComponent($component_name, $options);
$plugin = $entity->getRenderer($component_name);
$plugin_form =& $form['fields'][$field_name]['plugin']['settings_edit_form']['form'];
$subform_state = SubformState::createForSubform($plugin_form, $form, $form_state);
$plugin->submitConfigurationForm($plugin_form, $subform_state);
......@@ -475,22 +627,75 @@ class EntityCustomElementsDisplayEditForm extends EntityDisplayFormBase {
// @see \Drupal\custom_elements\Entity\EntityCeDisplay::getRenderer()
$options['configuration'] = array_diff_key(
$plugin->getConfiguration(),
array_flip(['field_definition', 'view_mode', 'name'])
array_flip(['field_definition', 'view_mode', 'name', 'is_slot'])
);
$form_state->set('plugin_settings_update', NULL);
}
$options['formatter'] = $values['formatter'];
$options['name'] = $values['name'];
$options['is_slot'] = (bool) $values['is_slot'];
$options['weight'] = $values['weight'];
$options['region'] = $values['region'];
$entity->setComponent($field_name, $options);
$entity->setComponent($component_name, $options);
}
}
$entity->setCustomElementName($form_values['custom_element_name']);
$entity->setForceAutoProcessing($form_values['force_auto']);
}
/**
* Renames CE display components according to form state info.
*
* This method must be called exactly once on $this->entity, each time a full
* form is submitted (and rebuilt on AJAX submit, or submitted/saved).
* Calling this more than once
* - may result in data corruption if a component is renamed to another
* existing component (if that component is also renamed at the same time;
* otherwise a validation error occurs and this is supposedly not called).
* - will result in warnings (but no data corruption), otherwise. (We can
* therefore assume that data corruption never occurs if no warnings are
* ever logged.)
*
* @param \Drupal\custom_elements\Entity\EntityCeDisplayInterface $entity
* The CE display whose components to rename. The component names must be
* equal to the 'existing_name' form state values, and will get new names
* if applicable.
* @param array $form
* A nested array of form elements comprising the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*
* @todo This setup fails when doing any editing after a rename. It should
* probably be replaced by a setup that keeps the original name in
* $this->entity (with all other changed config) and only does the renaming
* on 'real' submit.
*/
protected function renameEntityComponents(EntityCeDisplayInterface $entity, array $form, FormStateInterface $form_state) {
// 'Rename' components, in a way that components can switch names.
$new_components = [];
$form_values = $form_state->getValues();
// @todo Change this structure in #3446485: $form keys will be component
// names, not field names. Rename '#fields' (here and elsewhere)?
foreach ($form['#fields'] as $field_name) {
$values = $form_values['fields'][$field_name];
$component_name = $values['name'];
if ($values['region'] != 'hidden') {
if (isset($values['parent_wrapper']['existing_name'])) {
$existing_name = $values['parent_wrapper']['existing_name'];
if ($existing_name && $component_name !== $existing_name) {
// Old component can be empty (and populated later) if it used to
// be hidden.
$new_components[$component_name] = $entity->getComponent($existing_name) ?? [];
$entity->removeComponent($existing_name);
}
}
else {
// This should never happen. Are we being called twice?
$this->logger('custom_elements')->warning('Field row for @field_name somehow has no existing-name. (This suggests it is being renamed more than once.)', ['@field_name' => $field_name]);
}
}
}
foreach ($new_components as $component_name => $component) {
$entity->setComponent($component_name, $component);
}
}
/**
* {@inheritdoc}
*/
......@@ -551,7 +756,7 @@ class EntityCustomElementsDisplayEditForm extends EntityDisplayFormBase {
}
catch (\Exception $exception) {
$this->logger('custom_elements')
->warning($this->t('Error when determining compatible plugins: @error', ['@error' => $exception->getMessage()]));
->warning('Error when determining compatible plugins: @error', ['@error' => $exception->getMessage()]);
}
}
return $applicable_options;
......
......@@ -283,7 +283,7 @@ class CustomElementGenerator {
*
* Not accessible or empty fields are not built.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* The entity.
* @param \Drupal\custom_elements\CustomElement $custom_element
* The custom element.
......@@ -295,7 +295,7 @@ class CustomElementGenerator {
* (optional) The user for which to check access, or NULL to check access
* for the current user. Defaults to NULL.
*/
public function buildCeDisplay(EntityInterface $entity, CustomElement $custom_element, EntityCeDisplayInterface $display, string $view_mode, AccountInterface $account = NULL) {
public function buildCeDisplay(ContentEntityInterface $entity, CustomElement $custom_element, EntityCeDisplayInterface $display, string $view_mode, AccountInterface $account = NULL) {
$display->setOriginalMode($view_mode);
$custom_element->setTag($display->getCustomElementName());
......@@ -307,9 +307,15 @@ class CustomElementGenerator {
return;
}
// Else render using the individual display components.
foreach ($display->getComponents() as $field_name => $display_component) {
if ($this->fieldIsAccessible($entity, $field_name, $custom_element, $account)) {
if ($formatter = $display->getRenderer($field_name)) {
foreach ($display->getComponents() as $component_name => $display_component) {
if ($formatter = $display->getRenderer($component_name)) {
// @todo When implementing static properties per #3446287, 'field_name'
// is not necessarily present (or will be reused for the static value,
// so we don't have differing config schema?). Also, its method
// signatures will be different. Distinguish based on $formatter's
// interface.
$field_name = $display_component['field_name'];
if ($this->fieldIsAccessible($entity, $field_name, $custom_element, $account)) {
// @todo Move prepareBuild to prepareView phase.
$formatter->prepareBuild([$entity->get($field_name)]);
$formatter->build($entity->get($field_name), $custom_element);
......
......@@ -75,13 +75,6 @@ class EntityCeDisplay extends EntityDisplayBase implements EntityCeDisplayInterf
*/
protected CustomElementGenerator $ceGenerator;
/**
* {@inheritdoc}
*/
public function postCreate(EntityStorageInterface $storage) {
}
/**
* {@inheritdoc}
*/
......@@ -130,25 +123,57 @@ class EntityCeDisplay extends EntityDisplayBase implements EntityCeDisplayInterf
}
/**
* {@inheritdoc}
* Gets the formatter for a CE display component.
*
* @param string $field_name
* The name for the CE display component. NOTE this not the field name;
* it's only called $field_name because of the existing
* EntityDisplayInterfaceinterface. (Other implementations also effectively
* pass the 'component name', because the only components are fields.)
* @param bool $get_actual_field_name
* (Optional) Actually DO treat $field_name as a field name. DO NOT USE
* THIS; it's a temporary measure to support 'hidden'/new field rows in the
* UI, until they are removed / redone as component rows, in #3446485.
*
* @return \Drupal\custom_elements\CustomElementsFieldFormatterInterface|null
* A formatter plugin or NULL if the component does not exist.
*
* @todo remove $get_actual_field_name in #3446485.
*/
public function getRenderer($field_name) {
public function getRenderer($field_name, bool $get_actual_field_name = FALSE) {
if (isset($this->plugins[$field_name])) {
return $this->plugins[$field_name];
}
// Instantiate the formatter object from the stored display properties.
if (($component = $this->getComponent($field_name)) && isset($component['formatter']) && ($definition = $this->getFieldDefinition($field_name))) {
$component += ['configuration' => [], 'name' => $field_name, 'is_slot' => FALSE];
$formatter = $this->pluginManager->createInstance($component['formatter'], [
'field_definition' => $definition,
'view_mode' => $this->originalMode,
'name' => $component['name'],
'is_slot' => $component['is_slot'],
] + $component['configuration']
);
$component = NULL;
if (!$get_actual_field_name) {
$component_name = $field_name;
$component = $this->getComponent($component_name);
}
else {
$formatter = NULL;
// Instantiate the formatter object from the stored display properties.
$formatter = NULL;
if ($component && isset($component['formatter'])) {
// @todo When implementing static properties per #3446287, this code
// should know not to get a field name/definition, and the plugin
// manager should return something that is not a
// CustomElementsFieldFormatterInterface. Distinguish by the
// 'formatter' property having a prefix + colon.
// This is the actual field name.
if (!isset($component['field_name'])) {
// @todo Improve logging?
return NULL;
}
$definition = $this->getFieldDefinition($component['field_name']);
if ($definition) {
$component += ['configuration' => [], 'name' => $component_name, 'is_slot' => FALSE];
$formatter = $this->pluginManager->createInstance($component['formatter'], [
'field_definition' => $definition,
'view_mode' => $this->originalMode,
'name' => $component['name'],
'is_slot' => $component['is_slot'],
] + $component['configuration']
);
}
}
// Persist the formatter object.
......@@ -161,16 +186,21 @@ class EntityCeDisplay extends EntityDisplayBase implements EntityCeDisplayInterf
*/
public function getPluginCollections() {
$configurations = [];
foreach ($this->getComponents() as $field_name => $component) {
if (!empty($component['formatter']) && ($field_definition = $this->getFieldDefinition($field_name))) {
$component += ['configuration' => [], 'name' => $field_name, 'is_slot' => FALSE];
$configurations[$field_name] = [
'id' => $component['formatter'],
'field_definition' => $field_definition,
'view_mode' => $this->originalMode,
'name' => $component['name'],
'is_slot' => $component['is_slot'],
] + $component['configuration'];
foreach ($this->getComponents() as $component_name => $component) {
// @todo see getRenderer() when implementing #3446287; if() may need to
// change.
if (isset($component['field_name'])) {
$field_name = $component['field_name'];
if (!empty($component['formatter']) && ($field_definition = $this->getFieldDefinition($field_name))) {
$component += ['configuration' => [], 'name' => $component_name, 'is_slot' => FALSE];
$configurations[$field_name] = [
'id' => $component['formatter'],
'field_definition' => $field_definition,
'view_mode' => $this->originalMode,
'name' => $component['name'],
'is_slot' => $component['is_slot'],
] + $component['configuration'];
}
}
}
return [
......@@ -192,30 +222,66 @@ class EntityCeDisplay extends EntityDisplayBase implements EntityCeDisplayInterf
$this->setCustomElementName($custom_element->getPrefixedTag());
}
// Check if any components are present before doing other initialization.
$componentsPresent = !empty($this->content) || !empty($this->hidden);
if (!$componentsPresent) {
$initialized = !empty($this->content) || $this->forceAutoProcessing;
if (!$initialized) {
// Enable components options as done in the regular entity_view_display.
/** @var \Drupal\Core\Entity\Display\EntityViewDisplayInterface $entity_view_display */
$entity_view_display = \Drupal::service('entity_display.repository')
->getViewDisplay($this->targetEntityType, $this->bundle, $this->originalMode);
$field_definitions = \Drupal::service('entity_field.manager')->getFieldDefinitions($this->targetEntityType, $this->bundle);
// @todo Add support for statically set values.
// Enable every component with "auto" that is enabled in the display.
foreach ($entity_view_display->getComponents() as $name => $component) {
foreach ($entity_view_display->getComponents() as $field_name => $component) {
// Ignore extra-fields.
if (isset($field_definitions[$name])) {
$default_name = str_starts_with($name, 'field_') ? substr($name, strlen('field_')) : $name;
$this->setComponent($name, [
'name' => $default_name,
if (isset($field_definitions[$field_name])) {
$component_name = str_starts_with($field_name, 'field_') ? substr($field_name, strlen('field_')) : $field_name;
$this->setComponent($component_name, [
'field_name' => $field_name,
'formatter' => 'auto',
'weight' => $component['weight'],
'region' => 'content',
'is_slot' => str_starts_with($field_definitions[$name]->getType(), 'text') ? 1 : 0,
'region' => $this->getDefaultRegion(),
'is_slot' => str_starts_with($field_definitions[$field_name]->getType(), 'text') ? 1 : 0,
]);
}
}
}
$this->convertAlpha1Format();
}
/**
* Converts pre-alpha2 format. Temporary backward compatibility code.
*
* @deprecated in custom_elements:3.0.0-alpha2 and is removed from
* custom_elements:3.0.0-alpha3. May or may not be alpha4 or later but
* likely before beta1.
* @see https://www.drupal.org/project/custom_elements/issues/3455435
*
* @todo remove when implementing static properties per #3446287 or earlier.
*/
protected function convertAlpha1Format(): void {
// Either all components should be clearly old-format, or we exit silently.
$components = $this->getComponents();
if (!$components) {
return;
}
foreach ($components as $component_name => $component) {
if (isset($component['field_name']) || !isset($component['name'])) {
return;
}
}
// Always log, for as long as this method exists.
\Drupal::logger('custom_elements')->notice('Converted CE display @id from old format. Please change code / saved config.', ['@id' => $this->id() . ' ' . json_encode($component)]);
$this->content = [];
foreach ($components as $field_name => $component) {
$component_name = $component['name'];
unset($component['name']);
$component['field_name'] = $field_name;
$this->setComponent($component_name, $component);
}
unset($this->hidden);
}
/**
......@@ -228,11 +294,30 @@ class EntityCeDisplay extends EntityDisplayBase implements EntityCeDisplayInterf
$options['weight'] = isset($max) ? $max + 1 : 0;
}
$this->content[$name] = $options;
unset($this->hidden[$name]);
unset($this->plugins[$name]);
return $this;
}
/**
* {@inheritdoc}
*/
public function removeComponent($name) {
unset($this->content[$name]);
unset($this->plugins[$name]);
return $this;
}
/**
* {@inheritdoc}
*/
public function preSave(EntityStorageInterface $storage) {
// Work around some of the parent code.
$this->hidden = [];
parent::preSave($storage);
unset($this->hidden);
}
/**
* Gets the definitions of the fields that are candidate for display.
*/
......
......@@ -56,6 +56,9 @@ interface EntityCeDisplayInterface extends EntityDisplayInterface {
*
* @return \Drupal\custom_elements\CustomElementsFieldFormatterInterface|null
* The plugin, or NULL.
*
* @todo the return type will likely be 'widened' with #3446287 (static
* props); implement this before beta.
*/
public function getRenderer($field_name);
......
......@@ -81,13 +81,13 @@ class CustomElementsRenderMarkupTest extends BrowserTestBase {
$this->image->save();
$ce_display = $this->getCustomElementGenerator()->getEntityCeDisplay('node', 'article', 'default');
$ce_display->setComponent('field_paragraphs', [
'name' => 'paragraphs',
$ce_display->setComponent('paragraphs', [
'field_name' => 'field_paragraphs',
'formatter' => 'auto',
'is_slot' => TRUE,
]);
$ce_display->setComponent('field_teaser_media', [
'name' => 'teaser-media',
$ce_display->setComponent('teaser-media', [
'field_name' => 'field_teaser_media',
'formatter' => 'auto',
'is_slot' => TRUE,
]);
......@@ -97,13 +97,13 @@ class CustomElementsRenderMarkupTest extends BrowserTestBase {
$ce_display = $this->getCustomElementGenerator()->getEntityCeDisplay('paragraph', 'text', 'default');
$ce_display
->setCustomElementName('pg-text')
->setComponent('field_text', [
'name' => 'default',
->setComponent('default', [
'field_name' => 'field_text',
'formatter' => 'auto',
'is_slot' => TRUE,
])
->setComponent('field_title', [
'name' => 'title',
->setComponent('title', [
'field_name' => 'field_title',
'formatter' => 'auto',
'is_slot' => FALSE,
])
......@@ -161,8 +161,8 @@ class CustomElementsRenderMarkupTest extends BrowserTestBase {
*/
public function testNodeRendering() {
$ce_display = $this->getCustomElementGenerator()->getEntityCeDisplay('media', 'image', 'default');
$ce_display->setComponent('field_image', [
'name' => 'image',
$ce_display->setComponent('image', [
'field_name' => 'field_image',
'formatter' => 'auto',
'is_slot' => TRUE,
]);
......
......@@ -53,7 +53,7 @@ class EntityCeDisplayTest extends KernelTestBase {
// Check that arbitrary options are correctly stored.
$expected['component_1'] = [
'weight' => 10,
'name' => 'component',
'field_name' => 'component',
'region' => 'content',
'configuration' => [
'third_party_settings' => ['field_test' => ['foo' => 'bar']],
......@@ -94,8 +94,8 @@ class EntityCeDisplayTest extends KernelTestBase {
'bundle' => 'entity_test',
'mode' => 'default',
]);
$ce_display->setComponent('user_id', [
'name' => 'user',
$ce_display->setComponent('user', [
'field_name' => 'user_id',
'is_slot' => TRUE,
'formatter' => 'field:entity_reference_entity_view',
'configuration' => [
......@@ -105,7 +105,7 @@ class EntityCeDisplayTest extends KernelTestBase {
// Save and load to make sure config stays.
$ce_display->save();
$ce_display = EntityCeDisplay::load($ce_display->id());
$formatter = $ce_display->getRenderer('user_id');
$formatter = $ce_display->getRenderer('user');
$this->assertEquals('user', $formatter->getConfiguration()['name']);
$this->assertEquals(TRUE, $formatter->getConfiguration()['is_slot']);
$this->assertInstanceOf(CustomElementsFieldFormatterInterface::class, $formatter);
......
......@@ -135,12 +135,12 @@ class EntityReferenceCeFieldFormatterTest extends KernelTestBase {
'mode' => 'default',
])
->setComponent('name', [
'name' => 'name',
'field_name' => 'name',
'is_slot' => FALSE,
'formatter' => 'flattened',
])
->setComponent('ref_user', [
'name' => 'ref-user',
->setComponent('ref-user', [
'field_name' => 'ref_user',
'is_slot' => TRUE,
'formatter' => 'entity_ce_render',
'configuration' => [
......@@ -172,8 +172,8 @@ class EntityReferenceCeFieldFormatterTest extends KernelTestBase {
*/
protected function changeReferenceDisplayComponent(bool $is_slot, array $config = []): void {
EntityCeDisplay::load('user.user.default')
->setComponent('ref_user', [
'name' => 'ref-user',
->setComponent('ref-user', [
'field_name' => 'ref_user',
'is_slot' => $is_slot,
'formatter' => 'entity_ce_render',
'configuration' => $config + ['mode' => 'full'],
......@@ -185,6 +185,12 @@ class EntityReferenceCeFieldFormatterTest extends KernelTestBase {
* Tests output for an entityreference field with cardinality 1.
*/
public function testSingleCardinality() {
// $referrer:
// - name="referrer"
// - ref_user field = user with name="target"
// CE display:
// - name = username (flattened)
// - ref-user = entityref-formatter: view mode = "full"
$referrer = $this->setupUser();
// Slot.
......@@ -198,7 +204,7 @@ EOF;
$tested_markup = $this->renderCustomElement($custom_element);
$this->assertMarkupEquals($expected_markup, $tested_markup);
// No slot.
// No slot. (Change ref-user config to is_slot = FALSE).
$this->changeReferenceDisplayComponent(FALSE);
$data = htmlspecialchars(json_encode([
'element' => 'drupal-user',
......@@ -212,7 +218,7 @@ EOF;
$this->assertMarkupEquals($expected_markup, $tested_markup);
// Flatten: all referenced fields are added in the current tag, prefixed by
// the configured element name.
// the configured element name ("ref-user").
$this->changeReferenceDisplayComponent(FALSE, ['flatten' => TRUE]);
$expected_markup = <<<EOF
<drupal-user view-mode="full" name="referrer" ref-user-name="target"></drupal-user>
......@@ -226,15 +232,17 @@ EOF;
$tested_markup = $this->renderCustomElement($custom_element);
$this->assertMarkupEquals($expected_markup, $tested_markup);
// Rename component.
EntityCeDisplay::load('user.user.default')
->setComponent('name', [
'name' => 'user-name',
->removeComponent('name')
->setComponent('user-name', [
'field_name' => 'name',
'is_slot' => FALSE,
'formatter' => 'flattened',
])
->save();
$expected_markup = <<<EOF
<drupal-user view-mode="full" user-name="referrer" ref-user-user-name="target"></drupal-user>
<drupal-user view-mode="full" ref-user-user-name="target" user-name="referrer"></drupal-user>
EOF;
$custom_element = $this->getCustomElementGenerator()->generate($referrer, 'full');
$tested_markup = $this->renderCustomElement($custom_element);
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment