Skip to content

Add orcid sub-property type to compound metadata#7484

Open
ShanaLMoore wants to merge 21 commits into
nested-compound-metadata-foundationfrom
orcid-badge-compound-metadata
Open

Add orcid sub-property type to compound metadata#7484
ShanaLMoore wants to merge 21 commits into
nested-compound-metadata-foundationfrom
orcid-badge-compound-metadata

Conversation

@ShanaLMoore

@ShanaLMoore ShanaLMoore commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Summary

ORCID iD sub-property type for compound metadata. The iD renders as an inline
green badge next to a sibling sub-property (matching Zenodo); the sibling's
value links to a participant-name facet search.

Depends on #7479 (compound metadata foundation).

Issue:

Details

  • New type: orcid recognized by the schema, form, show renderer, and m3
    profile validator. No per-field Ruby.
  • Optional badge_for: <sibling> attaches the iD inline next to that sibling;
    falls back to a standalone labeled row when omitted or when the target is
    blank in the row.
  • Optional search_field: on string sub-properties wraps the show-page value
    in a catalog facet link (compound analogue of render_as: faceted).
  • Hyrax::OrcidSubpropertyValidator rejects malformed values on save; reuses
    the existing Hyrax::OrcidValidator regex.
  • Wired into the sample participants compound (name + ORCID, name
    facet-linked).
  • Works identically under HYRAX_FLEXIBLE=false and =true.
  • Locale keys propagated to de, es, fr, it, pt-BR, zh.

Testing

On a work or collection show page with a populated participant + ORCID:

  1. The name is a link; the green ORCID badge appears next to it.
  2. Clicking the name runs a participant-name facet search.
  3. Clicking the badge opens the author's ORCID profile.
Show page — work (HYRAX_FLEXIBLE=false, koppie) image
Show page — work (HYRAX_FLEXIBLE=true, allinson) image image
Click the name → participant-name facet search image
Click the badge → ORCID profile image

🤖 Assisted by Claude Code

CompoundSchema now threads a sub-property's badge_for: declaration
through into its normalized spec hash, alongside the existing type,
authority, index_keys, etc. Consumers can read both type: orcid and
the badge_for: sibling reference straight off the spec without
per-field Ruby.

The schema service stays the single source of truth for sub-property
shape; later commits read this through to render and validate.
orcid_badge(value, name:) returns a small linked ORCID iD badge
targeting https://orcid.org/<id>. It extracts the bare iD from
either the bare form or the URL form via the existing
Hyrax::OrcidValidator, and folds the sibling's name into the
accessibility label so screen readers and tooltips say "ORCID iD
for <name>".

The helper carries explicit i18n defaults so it works before its
locale keys are defined, keeping each commit self-contained.
CompoundAttributeRenderer now consumes a type: orcid sub-property
whose badge_for: names a sibling that has a value in the same
row, appending the linked ORCID badge inside the sibling's value
markup and omitting the orcid as its own labeled row. An orcid
with no badge_for: (or whose target is blank in that row) falls
back to rendering standalone via the value_markup case.

This matches Zenodo's "name [iD]" treatment exactly while leaving
the existing sub-property dispatch and the surrounding compound
markup untouched.
A new when 'orcid' case in the shared compound row partial renders
a text input with the bare-iD placeholder, accepting either the
0000-0000-0000-0000 form or the full https://orcid.org/ URL.
The subsequent validator normalizes either form on save.

The else branch (string default) is unchanged; orcid plugs in
through the same per-type dispatch as the other sub-property
types.
Hyrax::OrcidSubpropertyValidator is a record-level validator that
walks every compound row on a resource form, finds the type: orcid
sub-properties off the schema, and rejects any non-blank value
that does not match Hyrax::OrcidValidator::ORCID_REGEXP. Blank
values are allowed (required-ness lives in CompoundEntryValidator).

Errors land on :base with the same compound-naming convention
CompoundEntryValidator uses, so work and collection forms render
them cleanly. The validator is wired into ResourceForm alongside
CompoundEntryValidator.
CompoundValidator now rejects type: orcid sub-properties that
also declare authority: or values: (orcid is a free-text iD, not
a controlled vocabulary), and any badge_for: that points at
itself or at a property that is not a sibling sub-property of the
same compound parent.

A misconfigured m3 profile now fails at save with a clear message
instead of producing a misrouted or unrenderable badge.
The shipped participants compound now declares a type: orcid
sub-property bound to participant_name via badge_for:, so the
sample demonstrates the inline-badge treatment out of the box.

Mirrored in the non-flex schema (config/metadata/compound_metadata.yaml),
the central m3 profile, and the koppie m3 profile — the three
places PR #7479 ships participants. Dassie does not carry the
participants sample and is left untouched.
The badge helper, validator messages, form placeholder, and the
m3 profile compound validator now read through Hyrax's existing
i18n keys. This commit adds the English copies:

  - hyrax.compound_fields.orcid.{badge_alt,badge_aria,
    invalid,placeholder}
  - hyrax.flexible_schema_validators.compound_validator.errors.
    {orcid_with_option_source,orcid_badge_for_self,
    orcid_badge_for_unknown_sibling}

The participant_orcid sub-property label is intentionally left to
the humanized fallback ("Participant orcid"), matching the
treatment of the existing participant_title sub-property.
de, es, fr, it, pt-BR, and zh each pick up a compound_fields.orcid
block with locale-appropriate translations for badge_alt,
badge_aria, invalid, and placeholder. The non-English locales
otherwise carry no compound_fields keys (PR #7479's add to those
locales is still pending), so this commit adds the parent path
alongside the orcid sub-keys.

participant_orcid is intentionally not overridden; it humanizes
to "Participant orcid" in every locale.
The compound_fields documentation now lists orcid as a supported
sub-property type alongside string / url / work_or_url /
controlled, and adds a worked example section explaining the
badge_for: sibling binding, the show-page behavior, the standalone
fallback, the m3 profile validator rules, and the validators on
the persisted row.
orcid_badge previously called the bare image_tag helper. That
runs against whichever self the method is invoked on; from a
view it has the asset-pipeline context, but when included into
CompoundAttributeRenderer (a non-view Ruby object) it falls back
to a path that does not route through Sprockets and the badge
image 404s on the work show page.

ActionController::Base.helpers.image_tag returns a HelperProxy
that carries a full view-equivalent context, so the asset URL
resolves to /assets/orcid-<digest>.png regardless of caller.

The helper spec now requires a rooted asset path so the
regression is locked down.
A string sub-property can now declare `search_field: <solr_field>`;
on show, CompoundAttributeRenderer wraps the value in a link to
the catalog search filtered on that Solr field, matching the
scalar `render_as: faceted` affordance for compound rows.

The sample participants compound sets
`search_field: participant_name_sim` on participant_name, so
clicking the displayed name runs a search by that participant —
the Zenodo treatment now lines up: the name links to a search and
the inline ORCID badge links to the iD profile.
The m3 loader matches `available_on.class` by literal class name —
it does not expand `Hyrax::Work` to subclasses. As a result, the
sample participants compound was reaching CollectionResource and
GenericWork in koppie, but not Monograph (or Hyrax::Work-based
subclasses in the central profile).

Adding the concrete work classes to participants.available_on so
the sample compound (and its new orcid sub-property) reaches a
work in flex mode the same way it already does in non-flex.
@ShanaLMoore ShanaLMoore added the notes-minor Release Notes: Non-breaking features label Jun 9, 2026
Comment thread app/assets/javascripts/hyrax/compound_metadata.js Outdated
Comment thread app/assets/javascripts/hyrax/compound_metadata.js Outdated
Comment thread app/assets/stylesheets/hyrax/_compound_metadata.scss Outdated
Comment thread app/assets/stylesheets/hyrax/_compound_metadata.scss Outdated
Comment thread app/assets/stylesheets/hyrax/_compound_metadata.scss
Comment thread app/assets/stylesheets/hyrax/_compound_metadata.scss
Comment thread app/assets/stylesheets/hyrax/_compound_metadata.scss
Comment thread app/assets/stylesheets/hyrax/_compound_metadata.scss Outdated
Comment thread app/assets/stylesheets/hyrax/_compound_metadata.scss Outdated
Comment thread app/assets/stylesheets/hyrax/_compound_metadata.scss
@ShanaLMoore ShanaLMoore changed the base branch from main to nested-compound-metadata-foundation June 9, 2026 16:59
@ShanaLMoore ShanaLMoore requested a review from Copilot June 9, 2026 17:03

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds first-class support for type: orcid (and related display/validation options) to Hyrax compound metadata, enabling inline ORCID iD badges and optional faceted linking for sibling string sub-properties.

Changes:

  • Introduces type: orcid sub-properties with save-time format validation and show-page ORCID badge rendering (optionally inline via badge_for:).
  • Adds search_field: for string sub-properties to render show-page values as facet-filtered catalog links.
  • Updates sample compound metadata, m3 profile configuration, docs, styling, locales, and adds/extends specs for schema normalization, profile validation, rendering, and helpers.

Reviewed changes

Copilot reviewed 24 out of 24 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
spec/validators/hyrax/orcid_subproperty_validator_spec.rb Adds unit coverage for compound ORCID sub-property validation behavior.
spec/services/hyrax/flexible_schema_validators/compound_validator_spec.rb Extends flexible profile validation specs for type: orcid + badge_for: rules.
spec/services/hyrax/compound_schema_spec.rb Verifies schema normalization preserves badge_for and search_field.
spec/renderers/hyrax/renderers/compound_attribute_renderer_spec.rb Adds rendering expectations for ORCID badges and search_field facet links.
spec/helpers/hyrax/compound_fields_helper_spec.rb Tests orcid_badge helper output and accessibility/security attributes.
documentation/compound_fields.md Documents type: orcid, badge_for:, and search_field: semantics.
config/metadata/compound_metadata.yaml Wires ORCID + faceted participant name into the sample participants compound.
config/metadata_profiles/m3_profile.yaml Updates shipped m3 profile sample to include ORCID + search_field and class availability.
config/locales/hyrax.zh.yml Adds locale keys for ORCID badge/validation strings.
config/locales/hyrax.pt-BR.yml Adds locale keys for ORCID badge/validation strings.
config/locales/hyrax.it.yml Adds locale keys for ORCID badge/validation strings.
config/locales/hyrax.fr.yml Adds locale keys for ORCID badge/validation strings.
config/locales/hyrax.es.yml Adds locale keys for ORCID badge/validation strings.
config/locales/hyrax.en.yml Adds English locale keys for ORCID badge/validation strings and related compound messaging.
config/locales/hyrax.de.yml Adds locale keys for ORCID badge/validation strings.
app/views/hyrax/compounds/_compound_row.html.erb Adds form input rendering for type: orcid with placeholder.
app/validators/hyrax/orcid_subproperty_validator.rb Implements schema-driven validation of type: orcid sub-properties across compound rows.
app/services/hyrax/flexible_schema_validators/compound_validator.rb Adds flexible-profile validation for type: orcid option constraints and badge_for sibling rules.
app/services/hyrax/compound_schema.rb Extends normalized sub-property spec to include badge_for and search_field.
app/renderers/hyrax/renderers/compound_attribute_renderer.rb Renders ORCID badges (inline or standalone) and search_field facet links on show pages.
app/helpers/hyrax/compound_fields_helper.rb Adds orcid_badge helper to generate accessible, safe ORCID badge links.
app/forms/hyrax/forms/resource_form.rb Hooks the new ORCID sub-property validator into resource form validation.
app/assets/stylesheets/hyrax/_compound_metadata.scss Adds styling for the inline ORCID badge.
.koppie/config/metadata_profiles/m3_profile.yaml Mirrors m3 profile sample changes for the Koppie configuration set.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread app/renderers/hyrax/renderers/compound_attribute_renderer.rb
Comment thread app/views/hyrax/compounds/_compound_row.html.erb Outdated
Comment thread config/metadata_profiles/m3_profile.yaml Outdated
Comment on lines 869 to +873
label: Participants
title: Title
participant_name: Name
participant_role: Role
orcid:

@ShanaLMoore ShanaLMoore Jun 9, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Holding on this one. It was intentional decision to NOT add a hyrax.compound_fields.participants.participant_orcid locale override — letting the value humanize to "Participant orcid" in the rare standalone-fallback case (when badge_for: is omitted or the sibling has no value). The common case displays the inline green badge with no row label. Happy to add the label if reviewers prefer the explicit "ORCID iD" copy.

The sample participants compound now declares four sub-properties
(title, name, role, orcid), so the populator emits a four-key hash
per row — including participant_orcid: nil when the user did not
supply one. The validate + sync coverage in compound_metadata_form_spec
was written against the three-key shape; update the expectations to
include participant_orcid: nil so the assertions match the populator
output again.
Previously the renderer attached the badge whenever the badge_for
sibling had a value, even when the stored orcid value did not
parse to a bare iD. In that case orcid_badge returned nil and the
orcid sub-property was suppressed from its own row too, so the
stored value silently disappeared from the show page.

badge_attachments now requires Hyrax::OrcidValidator.extract_bare_orcid
to succeed before consuming the orcid row, and value_markup falls
back to escaped text when the badge cannot be built. Stored
malformed values surface in a fallback row instead of vanishing.
The previous comment claimed Hyrax::OrcidSubpropertyValidator
normalizes the value on save, but the validator only validates
the format. The show renderer is what derives the bare iD via
Hyrax::OrcidValidator at display time; the stored value is kept
as the user typed it. Update the form-row comment to match.
The central m3 profile uses GenericWorkResource for every other
available_on entry; the recently-added participants entry typed
GenericWork by mistake, leaving the sample compound unreachable
on the intended work class in flex mode.
hound flagged the new .hyrax-compound-orcid-badge img rule for
declaring height, width, vertical-align out of alphabetical
order. Reorder the trio.
Two style-only changes flagged on the inherited compound-metadata
JS by houndci, addressed here as a courtesy so this PR's CI is
clean:

- Wrap the IIFE invocation inside the outer parens (`}())` instead
  of `})()`), matching the `wrap-iife` style rule.
- Split the click handler into `handleRemoveClick` and
  `handleAddClick` and dispatch from a tiny outer listener, so no
  single function trips the cyclomatic-complexity rule.

Behavior is unchanged: the same remove-row and add-row gestures
fire the same DOM mutations, and the same `bindWorkOrUrlInputs`
runs on any new row.
houndci flagged property ordering on the inherited compound
metadata rules. Reorder the three blocks (.hyrax-compound-values,
the subproperty group, the entry-divider rule) alphabetically so
the warnings clear. The !important markers are intentional per
the file header comment (they override _collections.scss's
descendant flex/border styles), so they stay.
Comment thread app/assets/javascripts/hyrax/compound_metadata.js
Comment thread app/assets/stylesheets/hyrax/_compound_metadata.scss
Comment thread app/assets/stylesheets/hyrax/_compound_metadata.scss
Comment thread app/assets/stylesheets/hyrax/_compound_metadata.scss
Comment thread app/assets/stylesheets/hyrax/_compound_metadata.scss
@ShanaLMoore

Copy link
Copy Markdown
Contributor Author

houndci feedback addressed:

  • compound_metadata.js: IIFE wrap and click-handler complexity refactored in a44ffe6. Behavior unchanged — same remove-row / add-row dispatch, same select2 rebinding on new rows.
  • _compound_metadata.scss: property orderings alphabetized in 3a586c2.

Leaving alone:

  • The !important markers in .hyrax-compound-values are intentional per the file header comment (Add compound (hierarchical) metadata foundation #7479). They override _collections.scss's descendant div flex/border rules that would otherwise turn each compound entry into a bordered flex row. Removing them requires restructuring the collection-page CSS outside this PR's scope.

(git blame confirms the inherited rules come from PR #7479; hound surfaces them in this PR because the base branch hasn't merged yet.)

@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown

Test Results

    17 files  ±  0      17 suites  ±0   3h 39m 13s ⏱️ - 5m 12s
 7 796 tests + 36   7 489 ✅ + 36  307 💤 ±0  0 ❌ ±0 
26 528 runs  +150  25 933 ✅ +150  595 💤 ±0  0 ❌ ±0 

Results for commit 4d37dde. ± Comparison against base commit 20b843b.

This pull request removes 449 and adds 485 tests. Note that renamed tests count towards both.
spec.abilities.ability_spec ‑ Hyrax::Ability AdminSets and PermissionTemplates a user without edit access is expected not to be able to create #<Hyrax::PermissionTemplate:0x00007f0050ec8ae0>
spec.abilities.ability_spec ‑ Hyrax::Ability AdminSets and PermissionTemplates a user without edit access is expected not to be able to create #<Hyrax::PermissionTemplate:0x00007f5cdc91fc90>
spec.abilities.ability_spec ‑ Hyrax::Ability AdminSets and PermissionTemplates a user without edit access is expected not to be able to create #<Hyrax::PermissionTemplate:0x00007f8c92a2a140>
spec.abilities.ability_spec ‑ Hyrax::Ability AdminSets and PermissionTemplates a user without edit access is expected not to be able to create #<Hyrax::PermissionTemplate:0x00007f9251262588>
spec.abilities.ability_spec ‑ Hyrax::Ability AdminSets and PermissionTemplates a user without edit access is expected not to be able to create #<Hyrax::PermissionTemplateAccess:0x00007f0051237820>
spec.abilities.ability_spec ‑ Hyrax::Ability AdminSets and PermissionTemplates a user without edit access is expected not to be able to create #<Hyrax::PermissionTemplateAccess:0x00007f5cdc92c940>
spec.abilities.ability_spec ‑ Hyrax::Ability AdminSets and PermissionTemplates a user without edit access is expected not to be able to create #<Hyrax::PermissionTemplateAccess:0x00007f8c92a3dc18>
spec.abilities.ability_spec ‑ Hyrax::Ability AdminSets and PermissionTemplates a user without edit access is expected not to be able to create #<Hyrax::PermissionTemplateAccess:0x00007f92512894d0>
spec.abilities.ability_spec ‑ Hyrax::Ability AdminSets and PermissionTemplates a user without edit access is expected not to be able to destroy AdminSet: d3bc85a5-cd6e-447b-9728-46a3fe485919
spec.abilities.ability_spec ‑ Hyrax::Ability AdminSets and PermissionTemplates a user without edit access is expected not to be able to destroy Hyrax::AdministrativeSet: a2af5224-796f-4c04-9cce-a7402a782551
…
spec.abilities.ability_spec ‑ Hyrax::Ability AdminSets and PermissionTemplates a user without edit access is expected not to be able to create #<Hyrax::PermissionTemplate:0x00007f5f11665658>
spec.abilities.ability_spec ‑ Hyrax::Ability AdminSets and PermissionTemplates a user without edit access is expected not to be able to create #<Hyrax::PermissionTemplate:0x00007f7e9162a018>
spec.abilities.ability_spec ‑ Hyrax::Ability AdminSets and PermissionTemplates a user without edit access is expected not to be able to create #<Hyrax::PermissionTemplate:0x00007f8a204fdd58>
spec.abilities.ability_spec ‑ Hyrax::Ability AdminSets and PermissionTemplates a user without edit access is expected not to be able to create #<Hyrax::PermissionTemplate:0x00007fde39c3af18>
spec.abilities.ability_spec ‑ Hyrax::Ability AdminSets and PermissionTemplates a user without edit access is expected not to be able to create #<Hyrax::PermissionTemplateAccess:0x00007f5f11814da0>
spec.abilities.ability_spec ‑ Hyrax::Ability AdminSets and PermissionTemplates a user without edit access is expected not to be able to create #<Hyrax::PermissionTemplateAccess:0x00007f7e9163dc08>
spec.abilities.ability_spec ‑ Hyrax::Ability AdminSets and PermissionTemplates a user without edit access is expected not to be able to create #<Hyrax::PermissionTemplateAccess:0x00007f8a20a7e9d0>
spec.abilities.ability_spec ‑ Hyrax::Ability AdminSets and PermissionTemplates a user without edit access is expected not to be able to create #<Hyrax::PermissionTemplateAccess:0x00007fde3a078210>
spec.abilities.ability_spec ‑ Hyrax::Ability AdminSets and PermissionTemplates a user without edit access is expected not to be able to destroy AdminSet: 2e6874bb-ee8e-4cc8-a888-f915327fbf72
spec.abilities.ability_spec ‑ Hyrax::Ability AdminSets and PermissionTemplates a user without edit access is expected not to be able to destroy Hyrax::AdministrativeSet: 421fb1fd-436d-41e7-8602-9ee1f2232fae
…

♻️ This comment has been updated with latest results.

dassie + sirenia run Rails 6.1, where `link_to ... do ... end`
calls `capture(&block)` and requires `output_buffer=` on the
receiver. The renderer that includes `Hyrax::CompoundFieldsHelper`
is a plain Ruby object with no `output_buffer`, so the block form
raises `NoMethodError: undefined method output_buffer=`. (Rails 7
silently tolerates the missing setter, which is why koppie and
allinson were green.)

Switch to `ActionController::Base.helpers.link_to(content, url,
opts)` — a view-equivalent proxy that already carries
`output_buffer`, used in the non-block form so no `capture` runs.
`image_tag` already used the same proxy for asset path resolution,
so the helper is now uniformly view-context-free.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

notes-minor Release Notes: Non-breaking features

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants