diff --git a/.koppie/config/metadata_profiles/m3_profile.yaml b/.koppie/config/metadata_profiles/m3_profile.yaml index c8f77ec9fb..9934da94cf 100644 --- a/.koppie/config/metadata_profiles/m3_profile.yaml +++ b/.koppie/config/metadata_profiles/m3_profile.yaml @@ -938,6 +938,7 @@ properties: available_on: class: - GenericWork + - Monograph - CollectionResource data_type: array type: hash @@ -968,6 +969,7 @@ properties: group: identity required: true indexing: [participant_name_sim, participant_name_tesim] + search_field: participant_name_sim form: cols: 4 participant_role: @@ -979,6 +981,17 @@ properties: indexing: [participant_role_sim, participant_role_tesim] form: cols: 4 + # An ORCID iD sub-property. `badge_for:` binds the iD to a sibling so the + # show renderer appends an inline ORCID badge next to that sibling's value + # instead of rendering the iD as its own row. + participant_orcid: + type: orcid + subproperty_of: participants + group: identity + badge_for: participant_name + indexing: [participant_orcid_ssim] + form: + cols: 4 identifiers: available_on: class: diff --git a/app/assets/javascripts/hyrax/compound_metadata.js b/app/assets/javascripts/hyrax/compound_metadata.js index 3f09bb7ed6..57be417191 100644 --- a/app/assets/javascripts/hyrax/compound_metadata.js +++ b/app/assets/javascripts/hyrax/compound_metadata.js @@ -3,8 +3,8 @@ // rebinding. The sentinel guards against the IIFE running twice (Turbolinks // re-evaluates module scripts on some navigation paths), which would stack // duplicate listeners. Mirrors hyrax/redirects.js. -(function() { - if (document.hyraxCompoundsBound) return; +(function () { + if (document.hyraxCompoundsBound) { return; } document.hyraxCompoundsBound = true; // Bind select2 to a `work_or_url` sub-property. The v3 API (Hyrax bundles @@ -58,41 +58,51 @@ } document.addEventListener('turbolinks:load', bindAll); - document.addEventListener('click', function(event) { + // Remove a compound row: persisted rows are hidden + their `_destroy` flag + // flipped so the server-side populator drops them; unsaved rows are + // removed from the DOM outright. Returns true when the click was a + // remove-row gesture (so the dispatcher can stop further handling). + function handleRemoveClick(event) { var removeButton = event.target.closest('[data-hyrax-compound-remove-row]'); - if (removeButton) { - var row = removeButton.closest('[data-hyrax-compound-row]'); - if (!row) return; - var destroyFlag = row.querySelector('[data-hyrax-compound-destroy-flag]'); - if (destroyFlag && destroyFlag.value !== undefined) { - // Persisted rows: flip the _destroy flag and hide so the - // populator drops the row server-side. - destroyFlag.value = '1'; - row.style.display = 'none'; - } else { - row.parentNode.removeChild(row); - } - return; + if (!removeButton) { return false; } + var row = removeButton.closest('[data-hyrax-compound-row]'); + if (!row) { return true; } + var destroyFlag = row.querySelector('[data-hyrax-compound-destroy-flag]'); + if (destroyFlag && destroyFlag.value !== undefined) { + destroyFlag.value = '1'; + row.style.display = 'none'; + } else { + row.parentNode.removeChild(row); } + return true; + } + // Add a compound row: clone the section's template, swap the `__INDEX__` + // placeholder for a monotonic counter, append, and bind any select2 + // inputs the new row carries. Returns true when the click was an + // add-row gesture. + function handleAddClick(event) { var addButton = event.target.closest('[data-hyrax-compound-add-row]'); - if (!addButton) return; + if (!addButton) { return false; } var section = addButton.closest('[data-hyrax-compound]'); - if (!section) return; + if (!section) { return true; } var template = section.querySelector('[data-hyrax-compound-row-template]'); var rowsHost = section.querySelector('[data-hyrax-compound-rows]'); - if (!template || !rowsHost) return; + if (!template || !rowsHost) { return true; } - // Monotonic counter on the section; never recycle an index after a - // row is removed. Fallback to row count when the attribute is missing. var nextIndex = parseInt(section.dataset.nextIndex, 10); if (isNaN(nextIndex)) { nextIndex = rowsHost.querySelectorAll('[data-hyrax-compound-row]').length; } var html = template.innerHTML.replace(/__INDEX__/g, nextIndex); rowsHost.insertAdjacentHTML('beforeend', html); - // Bind select2 on any work_or_url inputs in the row just added. bindWorkOrUrlInputs(rowsHost.lastElementChild || rowsHost); section.dataset.nextIndex = String(nextIndex + 1); + return true; + } + + document.addEventListener('click', function (event) { + if (handleRemoveClick(event)) { return; } + handleAddClick(event); }); -})(); +}()); diff --git a/app/assets/stylesheets/hyrax/_compound_metadata.scss b/app/assets/stylesheets/hyrax/_compound_metadata.scss index ad1c9cdd06..b67648f77d 100644 --- a/app/assets/stylesheets/hyrax/_compound_metadata.scss +++ b/app/assets/stylesheets/hyrax/_compound_metadata.scss @@ -6,11 +6,11 @@ // in _collections.scss) that would otherwise turn each entry into a bordered // flex row. These rules override it regardless of source order. .hyrax-compound-values { + border: 0 !important; display: block !important; flex-direction: column; - border: 0 !important; - padding: 0 !important; max-width: 100%; + padding: 0 !important; // Force block flow on the entries/sub-properties; the label/value spans stay // inline so each sub-property reads as "Label: value" on one wrapping line. @@ -23,17 +23,17 @@ .hyrax-compound-subproperty, .hyrax-compound-subproperty-label, .hyrax-compound-subproperty-value { + background: none; border: 0 !important; - padding: 0 !important; margin: 0; - background: none; + padding: 0 !important; } // A light divider between entries. .hyrax-compound-entry + .hyrax-compound-entry { + border-top: 1px solid $gray-200 !important; margin-top: 0.5rem; padding-top: 0.5rem !important; - border-top: 1px solid $gray-200 !important; } .hyrax-compound-subproperty { @@ -48,3 +48,17 @@ margin-right: 0.25rem; } } + +// Small inline ORCID iD badge appended next to a sibling sub-property value +// when a `type: orcid` sub-property declares `badge_for:`. +.hyrax-compound-orcid-badge { + display: inline-block; + margin-left: 0.25rem; + vertical-align: baseline; + + img { + height: 1em; + vertical-align: -0.125em; + width: auto; + } +} diff --git a/app/forms/hyrax/forms/resource_form.rb b/app/forms/hyrax/forms/resource_form.rb index 2d7a65d572..bfd3023ba0 100644 --- a/app/forms/hyrax/forms/resource_form.rb +++ b/app/forms/hyrax/forms/resource_form.rb @@ -71,6 +71,7 @@ class ResourceForm < Hyrax::ChangeSet # rubocop:disable Metrics/ClassLength # `attributes:`), so it is replay-safe. validation(name: :default, inherit: true) do validates_with Hyrax::CompoundEntryValidator + validates_with Hyrax::OrcidSubpropertyValidator end ## diff --git a/app/helpers/hyrax/compound_fields_helper.rb b/app/helpers/hyrax/compound_fields_helper.rb index effc34590e..fd67e6628c 100644 --- a/app/helpers/hyrax/compound_fields_helper.rb +++ b/app/helpers/hyrax/compound_fields_helper.rb @@ -126,6 +126,44 @@ def compound_subproperty_label(compound_name, sub_property) default: sub_property.to_s.humanize) end + ## + # Renders a small linked ORCID iD badge. The href targets + # `https://orcid.org/`; the bare iD is extracted from either input form + # (bare or full URL) via {Hyrax::OrcidValidator.extract_bare_orcid}, so the + # caller does not need to normalize first. + # + # @param value [String, nil] a bare ORCID iD (`0000-0000-0000-0000`) or the + # full URL form + # @param name [String, nil] the sibling's display value; used in the + # accessibility / hover label ("ORCID iD for Hosseini, Mohammad"). + # When blank, falls back to the plain "ORCID iD" alt text. + # @return [ActiveSupport::SafeBuffer, nil] the badge HTML, or nil when + # +value+ is blank or unparseable. + def orcid_badge(value, name: nil) + bare_id = Hyrax::OrcidValidator.extract_bare_orcid(from: value.to_s) + return nil if bare_id.blank? + + alt = t('hyrax.compound_fields.orcid.badge_alt', default: 'ORCID iD') + label = name.present? ? t('hyrax.compound_fields.orcid.badge_aria', name: name, default: "ORCID iD for #{name}") : alt + # `ActionController::Base.helpers` is a view-equivalent proxy that + # resolves asset paths and tag helpers without needing the caller to + # carry `output_buffer`. Using it for both `image_tag` and `link_to` + # keeps the helper safe in either context: a view (where `self` already + # has output_buffer) or `Hyrax::Renderers::CompoundAttributeRenderer` + # (a non-view Ruby object). The non-block `link_to(content, url, opts)` + # form is required on Rails 6.1 — the block form calls `capture` which + # would raise `undefined method output_buffer=` on the renderer. + helpers = ActionController::Base.helpers + img = helpers.image_tag('orcid.png', alt: alt) + helpers.link_to(img, + "https://orcid.org/#{bare_id}", + class: 'hyrax-compound-orcid-badge', + target: '_blank', + rel: 'noopener noreferrer', + 'aria-label' => label, + title: label) + end + private # Options from a QA local authority. Uses {Hyrax::TolerantSelectService} so diff --git a/app/renderers/hyrax/renderers/compound_attribute_renderer.rb b/app/renderers/hyrax/renderers/compound_attribute_renderer.rb index 4807632b0a..aec0fd4b17 100644 --- a/app/renderers/hyrax/renderers/compound_attribute_renderer.rb +++ b/app/renderers/hyrax/renderers/compound_attribute_renderer.rb @@ -8,7 +8,10 @@ module Renderers # sub-properties produced by the SolrDocument `compound_attribute` reader — # and renders as a block of its populated sub-properties. Sub-property labels # come from the `hyrax.compound_fields..` i18n keys. - class CompoundAttributeRenderer < AttributeRenderer + class CompoundAttributeRenderer < AttributeRenderer # rubocop:disable Metrics/ClassLength + include ActionView::Helpers::AssetTagHelper + include Hyrax::CompoundFieldsHelper + def render return '' if blank_values? && !options[:include_empty] @@ -51,21 +54,35 @@ def entry_markup(entry) pairs = entry_to_pairs(entry) return '' if pairs.empty? - items = pairs.map { |sub_property, value| subproperty_markup(sub_property, value) }.join + bindings = badge_attachments(pairs) + visible = pairs.reject { |(sub_property, _)| bindings[:skip].include?(sub_property) } + return '' if visible.empty? + + items = visible.map do |sub_property, value| + subproperty_markup(sub_property, value, attached_orcid: bindings[:attach][sub_property]) + end.join %(
#{items}
) end - def subproperty_markup(sub_property, value) + def subproperty_markup(sub_property, value, attached_orcid: nil) label_html = ERB::Util.h(sub_property_label(sub_property)) + value_html = value_markup(sub_property, value).to_s + if attached_orcid.present? + badge = orcid_badge(attached_orcid, name: display_value(sub_property, value).to_s).to_s + value_html = "#{value_html} #{badge}" unless badge.empty? + end %(
) + %(#{label_html}: ) + - %(#{value_markup(sub_property, value)}) + + %(#{value_html}) + %(
) end - # Display markup for one sub-property value, by sub-property type: `url` and - # `work_or_url` are linked; otherwise escaped text with controlled ids - # translated to their term. + # Display markup for one sub-property value, by sub-property type: `url` + # and `work_or_url` are linked; `orcid` renders as a standalone badge + # (used when no `badge_for:` sibling is bound); otherwise escaped text + # with controlled ids translated to their term. A string sub-property + # that declares `search_field:` is wrapped in a facet search link to + # that Solr field, matching the scalar `render_as: faceted` behavior. def value_markup(sub_property, value) return ERB::Util.h(display_value(sub_property, value)) if value.blank? @@ -76,9 +93,51 @@ def value_markup(sub_property, value) work_or_url_markup(value) when 'controlled' controlled_markup(sub_property, value) + when 'orcid' + # When the value is unparseable, `orcid_badge` returns nil — fall + # back to escaped text so the stored value is still visible. + orcid_badge(value).presence || ERB::Util.h(value.to_s) else - ERB::Util.h(display_value(sub_property, value)) + string_markup(sub_property, value) + end + end + + # Plain string: facet-link the value when the sub-property opts in via + # `search_field:`, otherwise escape and emit verbatim. + def string_markup(sub_property, value) + spec = subproperty_spec(sub_property) + facet = spec.is_a?(Hash) ? spec[:search_field].to_s : '' + display = display_value(sub_property, value) + return ERB::Util.h(display) if facet.blank? + + link_to(ERB::Util.h(display), + Rails.application.routes.url_helpers + .search_catalog_path("f[#{ERB::Util.h(facet)}][]": value, locale: I18n.locale)) + end + + # For each row, decide which `type: orcid` sub-properties attach inline + # to a sibling rather than render as their own row. Returns + # `{ skip: [...], attach: { => } }`. + # An orcid is skipped (and the badge attached) only when its value + # parses to a bare iD via {Hyrax::OrcidValidator.extract_bare_orcid} — + # an unparseable value falls back to its own row so the stored value + # is still surfaced on the show page rather than silently dropped. + def badge_attachments(pairs) + return { skip: [], attach: {} } unless options[:subproperties].is_a?(Hash) + + by_key = pairs.to_h + skip = [] + attach = {} + pairs.each do |sub_property, value| + spec = subproperty_spec(sub_property) + next unless spec.is_a?(Hash) && spec[:type].to_s == 'orcid' + target = spec[:badge_for].to_s + next if target.blank? || by_key[target].blank? + next if Hyrax::OrcidValidator.extract_bare_orcid(from: value.to_s).blank? + skip << sub_property + attach[target] = value end + { skip: skip, attach: attach } end # A controlled value displays its authority/value-list term. When the diff --git a/app/services/hyrax/compound_schema.rb b/app/services/hyrax/compound_schema.rb index aede55fc9d..55b807957f 100644 --- a/app/services/hyrax/compound_schema.rb +++ b/app/services/hyrax/compound_schema.rb @@ -251,7 +251,9 @@ def normalize_subproperty(opts) required: truthy?(opts['required']), group: opts['group']&.to_s, cols: (form['cols'] || DEFAULT_COLS).to_i, - as: form['as']&.to_s } + as: form['as']&.to_s, + badge_for: opts['badge_for']&.to_s, + search_field: opts['search_field']&.to_s } end # Whether the compound itself is required (at least one row must exist). diff --git a/app/services/hyrax/flexible_schema_validators/compound_validator.rb b/app/services/hyrax/flexible_schema_validators/compound_validator.rb index 563756838c..615703f3e3 100644 --- a/app/services/hyrax/flexible_schema_validators/compound_validator.rb +++ b/app/services/hyrax/flexible_schema_validators/compound_validator.rb @@ -52,12 +52,50 @@ def validate_subproperty(name, config) return end - return unless config['type'].to_s == 'controlled' - return if config['authority'].present? || config['values'].present? + case config['type'].to_s + when 'controlled' then validate_controlled_source(name, config) + when 'orcid' then validate_orcid(name, config, parent_name) + end + end + def validate_controlled_source(name, config) + return if config['authority'].present? || config['values'].present? @errors << t('controlled_without_source', property: name) end + # An orcid sub-property must not declare an option source (it is a + # free-text iD, not a controlled vocabulary). Its optional `badge_for:` + # must name a sibling sub-property on the same compound parent — not + # itself, and not a property that lives outside the parent. + def validate_orcid(name, config, parent_name) + if config['authority'].present? || config['values'].present? + @errors << t('orcid_with_option_source', + property: name, + default: "Sub-property `#{name}` declares type: orcid and must not set authority: or values:.") + end + validate_badge_for(name, config, parent_name) + end + + def validate_badge_for(name, config, parent_name) + badge_for = config['badge_for'].to_s + return if badge_for.blank? + + if badge_for == name.to_s + @errors << t('orcid_badge_for_self', + property: name, + default: "Sub-property `#{name}` declares badge_for: pointing at itself.") + return + end + + target = properties[badge_for] + return if target.is_a?(Hash) && target['subproperty_of'].to_s == parent_name + + @errors << t('orcid_badge_for_unknown_sibling', + property: name, target: badge_for, parent: parent_name, + default: "Sub-property `#{name}` declares badge_for: `#{badge_for}`, " \ + "which is not a sibling sub-property of `#{parent_name}`.") + end + # A top-level `indexing:` on a parent would point the catalog at a # `_tesim` field the indexer never writes; indexing is declared # per subproperty. diff --git a/app/validators/hyrax/orcid_subproperty_validator.rb b/app/validators/hyrax/orcid_subproperty_validator.rb new file mode 100644 index 0000000000..911bb3a83b --- /dev/null +++ b/app/validators/hyrax/orcid_subproperty_validator.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +module Hyrax + # Validates every `type: orcid` sub-property value on a compound's persisted + # rows. A blank value is allowed (required-ness lives in + # {Hyrax::CompoundEntryValidator}); a non-blank value must match + # {Hyrax::OrcidValidator::ORCID_REGEXP} in either the bare-iD form + # (`0000-0000-0000-0000`) or the full `https://orcid.org/` URL form. + # + # Adds one error per offending row to `:base`, naming the compound and the + # offending sub-property — same keying convention {Hyrax::CompoundEntryValidator} + # uses so work and collection forms render the message cleanly. See + # documentation/compound_fields.md. + class OrcidSubpropertyValidator < ActiveModel::Validator + def validate(record) + schema = Hyrax::CompoundSchema.for(schema_source(record)) + schema.definitions.each do |name, definition| + next unless record.respond_to?(name) + validate_compound(record, name, definition) + end + rescue StandardError => e + Hyrax.logger.debug("OrcidSubpropertyValidator: #{e.message}") + end + + private + + # The schema declarations live on the underlying resource, not the form + # wrapper: a Reform form exposes it as `model`. Fall back to the record + # itself (e.g. a plain ActiveModel object in unit tests). + def schema_source(record) + record.respond_to?(:model) ? record.model : record + end + + def validate_compound(record, name, definition) + orcid_keys = definition[:subproperties].select { |_key, spec| spec[:type].to_s == 'orcid' }.keys + return if orcid_keys.empty? + + Array(record.public_send(name)).each do |entry| + next unless entry.is_a?(Hash) + orcid_keys.each do |sub_property| + value = entry[sub_property] || entry[sub_property.to_sym] + next if value.blank? + next if Hyrax::OrcidValidator::ORCID_REGEXP.match?(value.to_s) + record.errors.add(:base, message_for(name, sub_property, value)) + end + end + end + + def message_for(name, sub_property, value) + I18n.t('hyrax.compound_fields.orcid.invalid', + compound: compound_label(name), + field: subproperty_label(name, sub_property), + value: value, + default: %("#{value}" is not a valid ORCID iD for #{compound_label(name)} > #{subproperty_label(name, sub_property)}.)) + end + + def compound_label(name) + I18n.t("hyrax.compound_fields.#{name}.label", default: name.to_s.humanize) + end + + def subproperty_label(compound_name, sub_property) + I18n.t("hyrax.compound_fields.#{compound_name}.#{sub_property}", default: sub_property.to_s.humanize) + end + end +end diff --git a/app/views/hyrax/compounds/_compound_row.html.erb b/app/views/hyrax/compounds/_compound_row.html.erb index b771fa36d3..a549aaa375 100644 --- a/app/views/hyrax/compounds/_compound_row.html.erb +++ b/app/views/hyrax/compounds/_compound_row.html.erb @@ -10,8 +10,8 @@ row_label_singular - "Contributor", "License", etc. Each sub-property renders according to its declared `type:` (string, - controlled, url, work_or_url). db_table / geocode are future types; the - `else` branch is the extension seam. + controlled, url, work_or_url, orcid). db_table / geocode are future types; + the `else` branch is the extension seam. %> <% subproperties = definition[:subproperties] %> <% groups = definition[:groups] %> @@ -56,6 +56,16 @@ <%= url_field_tag input_name, value, id: input_id, class: 'form-control form-control-sm' %> + <% when 'orcid' %> + <%# A bare iD (0000-0000-0000-0000) or the full https://orcid.org/ URL. + Hyrax::OrcidSubpropertyValidator accepts either form (the show + renderer extracts the bare iD via Hyrax::OrcidValidator); + the stored value is kept as the user typed it. %> + <%= text_field_tag input_name, value, + id: input_id, + class: 'form-control form-control-sm', + placeholder: t('hyrax.compound_fields.orcid.placeholder', default: '0000-0000-0000-0000'), + autocomplete: 'off' %> <% when 'work_or_url' %> <%# select2 v3 (bound in compound_metadata.js) binds to this hidden input; `data-label` pre-seeds the displayed text. %> diff --git a/config/locales/hyrax.de.yml b/config/locales/hyrax.de.yml index f8e7dd2604..b432114407 100644 --- a/config/locales/hyrax.de.yml +++ b/config/locales/hyrax.de.yml @@ -864,6 +864,12 @@ de: number_of_results_to_display_per_page: Anzahl der Ergebnisse die pro Seite angezeigt werden results_per_page: "Ergebnisse pro Seite:" sort_by_html: "Sortieren nach:" + compound_fields: + orcid: + badge_alt: ORCID iD + badge_aria: ORCID iD von %{name} + invalid: "%{compound} > %{field}: \"%{value}\" ist keine gültige ORCID iD" + placeholder: 0000-0000-0000-0000 contact_form: button_label: Senden email_label: Ihre E-Mail diff --git a/config/locales/hyrax.en.yml b/config/locales/hyrax.en.yml index 1398ec48b2..6da063c36b 100644 --- a/config/locales/hyrax.en.yml +++ b/config/locales/hyrax.en.yml @@ -870,6 +870,11 @@ en: title: Title participant_name: Name participant_role: Role + orcid: + badge_alt: ORCID iD + badge_aria: ORCID iD for %{name} + invalid: "%{compound} > %{field}: \"%{value}\" is not a valid ORCID iD" + placeholder: 0000-0000-0000-0000 identifiers: label: Identifiers identifier: Identifier @@ -1420,6 +1425,9 @@ en: unknown_parent: "m3 profile subproperty `%{property}` declares `subproperty_of: %{parent}`, but `%{parent}` is not a declared `type: hash` compound property" controlled_without_source: "m3 profile compound subproperty `%{property}` is `type: controlled` but declares neither `authority` nor `values`" top_level_indexing: "m3 profile compound property `%{property}` must not declare top-level `indexing`; declare `indexing` (or `index_keys`) on each subproperty instead" + orcid_with_option_source: "m3 profile compound subproperty `%{property}` is `type: orcid` and must not declare `authority` or `values` (ORCID iDs are free-text, not a controlled vocabulary)" + orcid_badge_for_self: "m3 profile compound subproperty `%{property}` declares `badge_for: %{property}` (it cannot point at itself)" + orcid_badge_for_unknown_sibling: "m3 profile compound subproperty `%{property}` declares `badge_for: %{target}`, but `%{target}` is not a sibling sub-property of `%{parent}`" redirects_validator: errors: invalid_available_on: "m3 profile `redirects` property must be available on at least one work or collection class declared in this profile" diff --git a/config/locales/hyrax.es.yml b/config/locales/hyrax.es.yml index 851845b82e..b7a20b824e 100644 --- a/config/locales/hyrax.es.yml +++ b/config/locales/hyrax.es.yml @@ -870,6 +870,12 @@ es: number_of_results_to_display_per_page: Número de resultados a mostrar por página results_per_page: "Resultados por página:" sort_by_html: "Tipo:" + compound_fields: + orcid: + badge_alt: ORCID iD + badge_aria: ORCID iD de %{name} + invalid: "%{compound} > %{field}: \"%{value}\" no es un ORCID iD válido" + placeholder: 0000-0000-0000-0000 contact_form: button_label: Enviar email_label: Tu Correo Electrónico diff --git a/config/locales/hyrax.fr.yml b/config/locales/hyrax.fr.yml index dd030b9502..6392aa1800 100644 --- a/config/locales/hyrax.fr.yml +++ b/config/locales/hyrax.fr.yml @@ -871,6 +871,12 @@ fr: number_of_results_to_display_per_page: Nombre de résultats à afficher par page results_per_page: "Résultats par page:" sort_by_html: "Trier par:" + compound_fields: + orcid: + badge_alt: Identifiant ORCID + badge_aria: Identifiant ORCID de %{name} + invalid: "%{compound} > %{field} : \"%{value}\" n'est pas un identifiant ORCID valide" + placeholder: 0000-0000-0000-0000 contact_form: button_label: Envoyer email_label: Votre Email diff --git a/config/locales/hyrax.it.yml b/config/locales/hyrax.it.yml index 5152353b2e..b8be269201 100644 --- a/config/locales/hyrax.it.yml +++ b/config/locales/hyrax.it.yml @@ -870,6 +870,12 @@ it: number_of_results_to_display_per_page: Numero di risultati da visualizzare per pagina results_per_page: "Risultati per la pagina:" sort_by_html: "Ordina per:" + compound_fields: + orcid: + badge_alt: ORCID iD + badge_aria: ORCID iD di %{name} + invalid: "%{compound} > %{field}: \"%{value}\" non è un ORCID iD valido" + placeholder: 0000-0000-0000-0000 contact_form: button_label: Inviare email_label: La tua email diff --git a/config/locales/hyrax.pt-BR.yml b/config/locales/hyrax.pt-BR.yml index c376750641..70c2b313ca 100644 --- a/config/locales/hyrax.pt-BR.yml +++ b/config/locales/hyrax.pt-BR.yml @@ -863,6 +863,12 @@ pt-BR: number_of_results_to_display_per_page: Número de resultados exibidos por página results_per_page: "Resultados por página:" sort_by_html: "Ordenar por:" + compound_fields: + orcid: + badge_alt: ORCID iD + badge_aria: ORCID iD de %{name} + invalid: "%{compound} > %{field}: \"%{value}\" não é um ORCID iD válido" + placeholder: 0000-0000-0000-0000 contact_form: button_label: Enviar email_label: Seu email diff --git a/config/locales/hyrax.zh.yml b/config/locales/hyrax.zh.yml index 2657c06f01..2cf5541c80 100644 --- a/config/locales/hyrax.zh.yml +++ b/config/locales/hyrax.zh.yml @@ -868,6 +868,12 @@ zh: number_of_results_to_display_per_page: 数量的结果显示,每页 results_per_page: 每页结果 sort_by_html: "<跨>排序:" + compound_fields: + orcid: + badge_alt: ORCID iD + badge_aria: "%{name} 的 ORCID iD" + invalid: "%{compound} > %{field}:\"%{value}\" 不是有效的 ORCID iD" + placeholder: 0000-0000-0000-0000 contact_form: button_label: 发送 email_label: 您的邮件 diff --git a/config/metadata/compound_metadata.yaml b/config/metadata/compound_metadata.yaml index 6725d95b44..adb1af1094 100644 --- a/config/metadata/compound_metadata.yaml +++ b/config/metadata/compound_metadata.yaml @@ -57,6 +57,10 @@ attributes: group: identity required: true index_keys: [participant_name_sim, participant_name_tesim] + # `search_field:` opts a string sub-property into faceted-link display: + # on show, the value renders as a link to a catalog search filtered on + # the named Solr field, matching the scalar `render_as: faceted` path. + search_field: participant_name_sim form: cols: 4 # A controlled sub-property can take its options from an inline list (below) @@ -70,6 +74,19 @@ attributes: index_keys: [participant_role_sim, participant_role_tesim] form: cols: 4 + # An ORCID iD sub-property. `badge_for:` binds the iD to a sibling so the + # show renderer appends an inline ORCID badge next to that sibling's value + # instead of rendering the iD as its own row. Accepts either the bare iD + # (0000-0000-0000-0000) or the full https://orcid.org/ URL; the validator + # rejects anything else. + participant_orcid: + type: orcid + subproperty_of: participants + group: identity + badge_for: participant_name + index_keys: [participant_orcid_ssim] + form: + cols: 4 # A typed identifier: an open-entry value plus a controlled identifier type. # Named `identifiers` (plural) to avoid colliding with the scalar `identifier` diff --git a/config/metadata_profiles/m3_profile.yaml b/config/metadata_profiles/m3_profile.yaml index d9f3494e53..1dd828f569 100644 --- a/config/metadata_profiles/m3_profile.yaml +++ b/config/metadata_profiles/m3_profile.yaml @@ -962,6 +962,8 @@ properties: available_on: class: - Hyrax::Work + - Monograph + - GenericWorkResource - CollectionResource data_type: array type: hash @@ -994,6 +996,7 @@ properties: group: identity required: true indexing: [participant_name_sim, participant_name_tesim] + search_field: participant_name_sim form: cols: 4 participant_role: @@ -1005,6 +1008,17 @@ properties: indexing: [participant_role_sim, participant_role_tesim] form: cols: 4 + # An ORCID iD sub-property. `badge_for:` binds the iD to a sibling so the + # show renderer appends an inline ORCID badge next to that sibling's value + # instead of rendering the iD as its own row. + participant_orcid: + type: orcid + subproperty_of: participants + group: identity + badge_for: participant_name + indexing: [participant_orcid_ssim] + form: + cols: 4 identifiers: available_on: class: diff --git a/documentation/compound_fields.md b/documentation/compound_fields.md index 6cdce4ca24..0e9d42d6e0 100644 --- a/documentation/compound_fields.md +++ b/documentation/compound_fields.md @@ -112,10 +112,11 @@ textarea). Supported `type:` values: | `type:` | Renders as | Notes | |--------------|---------------------|-------| -| `string` | text input | The default when `type:` is omitted. | +| `string` | text input | The default when `type:` is omitted. Add `search_field: ` to wrap the show-page value in a link to a catalog search filtered on that Solr field (same affordance the scalar `render_as: faceted` path provides). | | `url` | url input → auto-linked on show | The stored value is rendered as a clickable `` on show pages (matching the scalar `render_as: external_link` behavior). | | `work_or_url` | select2 typeahead → linked on show | Searches internal works (via the `compound_works` QA authority, backed by `Hyrax::CompoundWorkPickerBuilder`) **or** accepts a typed external URL. The stored value is a work id or a URL; on show, a work id links to the work's show page with its title (resolved by `Hyrax::CompoundWorkResolver`), a URL is auto-linked. | | `controlled` | `