Annotations UI — Developer Reference
Developer-focused reference. For module overview, pages, and permissions see README.md.
Overview slot storage
The add-new page always shows an Overview row first — the bundle-level slot covering the target as a whole rather than any specific field. In storage this is an annotation entity with field_name = ''. The add-new page URL uses _overview as the field name parameter; the controller converts it to the empty-string sentinel when creating the entity.
Drupal\Core\Field\Plugin\Field\FieldType\StringItem::isEmpty() treats '' as NULL, so queries for bundle-level annotations must use IS NULL rather than = ''. AnnotationStorageService handles this internally.
AnnotationFieldFilter
The Field exposed filter on the annotations_target view (AnnotationFieldFilter) scopes its options to the current target by reading $this->view->argument['target_id']. If the view is altered and that argument is renamed or removed, the filter falls back to showing fields from all targets.
Translation internals
annotation has translatable = TRUE in its entity annotation. The four-table schema (annotation, annotation_field_data, annotation_revision, annotation_field_revision) is always created on install — the schema is fixed by the entity definition, not by whether content_translation is installed.
AnnotationStorageService is language-aware: all read methods accept an optional $langcode parameter, falling back to the current content language, then to the default translation. Consumer contexts (overlays, context assembly, coverage) always receive the appropriate language without needing to pass a langcode explicitly.
entityTypeAlter() registers both default and edit form classes pointing to AnnotationEditForm. Both must be present: content_translation calls getFormObject('default') when rendering the "Add translation" form — without it the entity throws InvalidPluginDefinitionException.
Revision routing
annotation routes are auto-generated by route providers registered in AnnotationsUiHooks::entityTypeAlter(). There are no hand-defined entity routes in annotations_ui.routing.yml.
RevisionHtmlRouteProvider auto-generates four routes using stock Drupal controllers:
version-history— revision table with revert/delete operation linksrevision— single revision viewrevision-revert-form— confirmation form for reverting to a revisionrevision-delete-form— confirmation form for deleting a revision
Viewing revision history requires view annotation revisions (or edit any annotation). Revert and delete-revision operations require edit any annotation only — they are destructive and intentionally restricted to admins.
content_moderation state cleanup
AnnotationsUiHooks::annotationDelete() contains a workaround for a core bug: EntityOperations::entityDelete() in content_moderation queries content_moderation_state by content_entity_revision_id (the currently loaded revision). Because each new moderated revision creates a new content_moderation_state entity rather than a new revision of the existing one, only the current revision's record is found and deleted — all other revision records become orphans.
The hook implements hook_ENTITY_TYPE_delete() (annotation_delete) rather than the generic hook_entity_delete(). Core always invokes entity-type-specific delete hooks before the generic group (EntityStorageBase::invokeHook()), so our cleanup is guaranteed to run before content_moderation's EntityOperations::entityDelete() regardless of module weight — it deletes every content_moderation_state entity that references the deleted annotation by entity ID, so content_moderation's own query finds nothing.
Uninstall cleanup lives in the root annotations module (which owns the entity type and schema), not here: annotations_uninstall() purges leftover content_moderation_state orphans before dropping the annotation tables. Uninstalling annotations_ui alone never touches annotation data.
If core ever fixes EntityOperations::entityDelete() to query by content_entity_id rather than content_entity_revision_id, remove the hook and the @todo in the docblock.