chore(Internal HTTP API): #3591669 Heuristics for the content-entity-reference selection UI

Closes #3591669 (closed)

What this MR does

Applies heuristics to the content-entity-reference selection UI ("Typed Data browser" picker) so it only offers data a Code Component Developer can meaningfully consume, and mirrors the same rules at save time so storage rejects what the picker hides.

  1. Image field (ApiUiContentEntityReferenceControllers::buildFieldEntry()): the picker listed the same image URL twice — src and the src_with_alternate_widths it is cloned from — plus srcset_candidate_uri_template, a raw RFC 6570 URI template. It now offers only the developer-facing src, relabelled "Image URL" in ImageItemOverride, and excludes the two implementation-detail computed properties by name. Done picker-side only, so PropSourceSuggester keeps offering them (it has different needs).
  2. Translation/revision metadata (ApiUiContentEntityReferenceControllers::listFields()): a new isConsideredIrrelevant() heuristic hides default_langcode, revision_log, revision_translation_affected, and the content_translation_source/content_translation_outdated base fields. revision_translation_affected is resolved via $entity_type->getKey('revision_translation_affected') — a default entity key like default_langcode, not something getRevisionMetadataKey() resolves.
  3. Formatted text fields (text, text_long, text_with_summary): hide the raw value/format/summary, keeping only the computed processed/summary_processed.
  4. Raw, non-navigable URIs: any uri-typed property lacking a UriSchemeConstraint restricted to http/https is hidden — it is not guaranteed to be browser-navigable. This started as a link-field-specific check (raw uri, e.g. entity:node/1) and was generalized into a shape-based rule, which also caught a leak the by-class check missed: the file entity's own raw stream-wrapper uri (e.g. public://image.jpg).
  5. Datetime fields: computed DrupalDateTime-object properties (date, start_date, end_date) crashed rendering with a preg_match() TypeError when picked. Fixed as a consequence of item 6 below (they are explicitly marked internal, so the corrected internal-property rule now hides them); no datetime-specific guard was needed once that was fixed.
  6. isInternal() computed-property blind spot: DataDefinitionInterface::isInternal() cannot distinguish an explicit ->setInternal(TRUE) from the value it defaults to for computed properties — so isInternal() && !isComputed() silently failed to exclude a property that is both computed and explicitly internal (e.g. date, via DateTimeItemOverride). Fixed by inspecting the raw definition array (TypedDataHelper::isExplicitlyInternal()/isEffectivelyInternal()), converging the picker, EntityFieldPropSourceMatcher, and the storage-level validator onto shared helpers. A new PHPStan rule (RequireTypedDataHelperForInternalCheckRule) forbids direct isInternal()/['internal'] checks outside TypedDataHelper going forward.
  7. Storage-level enforcement: EntityFieldExpressionMustNotTargetInternalPropertyConstraint had the same blind spot as item 6, so a hand-crafted expression targeting date was never actually rejected at save time before this fix. A new, equivalent constraint, EntityFieldExpressionMustNotTargetRawUriProperty, mirrors item 4's rule at save time.

Each concern is in a separate commit for reviewability.

Testing steps

  • Apply the branch, enable the canvas_dev_er module (the feature flag for content-entity-reference props) and clear caches.
  • In the code editor, add a Content entity reference prop and browse into a bundle that has an image field.
  • Confirm the image field offers a single "Image URL" property — not "Resolved image URL with ?alternateWidths query parameter", a duplicate, or the srcset template.
  • Browse into a translatable, revisionable content type (e.g. an Article with translation enabled).
  • Confirm "Default translation", "Revision translation affected", "Translation source", "Translation outdated" and "Revision log message" are not offered, while real fields (including the entity's own langcode) still are.
  • Browse into a bundle with a formatted text field (text/text_long/text_with_summary); confirm only processed/summary_processed are offered, not the raw value/format/summary.
  • Browse into a bundle with a link field; confirm only url/title/options are offered, not the raw uri.
  • Browse into an image field's referenced file entity; confirm only the computed url is offered for the file's own uri field, not the raw stream-wrapper value.
  • Browse into a bundle with a datetime or daterange field; confirm the computed date/start_date/end_date are not offered.
  • Automated coverage: ApiUiContentEntityReferenceControllersTest, JavaScriptComponentValidationTest (internal-property and raw-URI entityFields rejection cases), PropExpressionKernelTest.

Notes for reviewers

  • Hiding the image twins is done in the controller, not at the field-type level: marking them internal there would also hide them from PropSourceSuggester, which still relies on src_with_alternate_widths and srcset_candidate_uri_template. 'is source for' can't drive it either — the matcher reads it the other way around (it hides src).
  • The image twins are excluded by name (not "keep only src") so any future image property stays offered by default.
  • src_with_alternate_widths is slated for removal in #3591648.
  • PropSourceSuggester isn't touched by the raw-URI generalization (item 4): it doesn't yet hide content_translation_source/content_translation_outdated either, and the two surfaces' isConsideredIrrelevant() methods were deliberately not forced into one shared implementation — they serve different audiences and may diverge further. Left as a follow-up, not in scope here.

AI usage

AI-Generated: Yes (used Claude Code to plan, implement and write tests).

Edited by Christian López Espínola

Merge request reports

Loading