Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
f8521e4
Add compound metadata schema reader and flag
laritakr Jun 4, 2026
f8e1f04
Render compound metadata fields on forms
laritakr Jun 4, 2026
9f7d349
Index and reload compound metadata fields
laritakr Jun 4, 2026
c5c8637
Render compound metadata on show pages
laritakr Jun 4, 2026
9f36077
Ship sample compound metadata on Hyrax
laritakr Jun 4, 2026
dbc22f2
Document compound metadata fields
laritakr Jun 4, 2026
f23f344
Rename agent sample compound to agents
laritakr Jun 4, 2026
5b8b54f
Fix compound metadata display on collections
laritakr Jun 4, 2026
a4363ab
Validate required compound sub-fields
laritakr Jun 4, 2026
bb0ca49
Resolve compound card schema from Solr doc
laritakr Jun 4, 2026
70cc171
Show collection compounds in flexible mode
laritakr Jun 4, 2026
c341780
Build collection terms on the class term list
laritakr Jun 5, 2026
18777a1
Fix compound card display in flexible mode
laritakr Jun 5, 2026
62d1e90
Drop duplicate label from compound cards
laritakr Jun 5, 2026
ff1b7e9
Align collection metadata values past long URLs
laritakr Jun 5, 2026
032264b
Improve compound metadata show rendering
laritakr Jun 5, 2026
d1458c0
Dynamic solr doc method creation for compounds
laritakr Jun 5, 2026
803338e
Rename the sample agents compound to participants
laritakr Jun 5, 2026
4b52b20
Define real SolrDocument readers for compounds
laritakr Jun 5, 2026
d82788a
Show collections with declared but empty compounds
laritakr Jun 5, 2026
a54d3f7
Rename compound "subfield" to "subproperty"
laritakr Jun 6, 2026
e0766b3
Declare compound subproperties as composable properties
laritakr Jun 6, 2026
5505a46
Support flat compound subproperties in flexible schemas
laritakr Jun 6, 2026
ccea414
Link and search collections in work_or_url compounds
laritakr Jun 6, 2026
57ceb21
Place compounds by their form primary flag
laritakr Jun 6, 2026
13f2f1e
Merge branch 'main' into rework-yaml-structure
laritakr Jun 6, 2026
d2f95a7
Guard nil flexible schema when resolving attributes
laritakr Jun 6, 2026
845551f
Share row-coercion plumbing across field behaviors
laritakr Jun 6, 2026
654c03d
Clean up compound vs. field-behavior documentation
laritakr Jun 6, 2026
5f1cfdd
Merge pull request #7483 from samvera/rework-yaml-structure
laritakr Jun 6, 2026
fe4aece
Remove config flag for compound metadata
laritakr Jun 6, 2026
fafb610
Clean up a few comments
laritakr Jun 6, 2026
20b843b
Re-render Valkyrie work form on failed validation
laritakr Jun 8, 2026
197e231
Apply flexible profile changes to compound forms
laritakr Jun 9, 2026
b1270fd
Declare compound subproperties on the child
laritakr Jun 10, 2026
65b09cc
Derive compound sub-property Solr field names
laritakr Jun 11, 2026
b3ec8f0
Reject duplicate sub-property names in a compound
laritakr Jun 11, 2026
7880819
Translate compound validator errors to locales
laritakr Jun 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 189 additions & 1 deletion .koppie/config/metadata_profiles/m3_profile.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -927,4 +927,192 @@ properties:
display: true
primary: false
range: http://www.w3.org/2001/XMLSchema#string
property_uri: http://vocabulary.samvera.org/ns#transcriptIds
property_uri: http://vocabulary.samvera.org/ns#transcriptIds
# Sample compound (hierarchical) metadata, demonstrating the Hyrax compound
# foundation in flexible mode. A compound is a `type: hash, multiple: true`
# parent; its members are declared as separate properties naming it via
# `available_on: { properties: [<parent>] }`, each carrying its own indexing
# and form placement. A member may name more than one parent, and `name:` sets
# its key inside the compound. Such members are not standalone resource
# attributes. See documentation/compound_fields.md.
participants:
available_on:
class:
- GenericWork
- CollectionResource
data_type: array
type: hash
range: http://www.w3.org/2001/XMLSchema#string
display_label:
default: Participants
form:
primary: true
property_uri: http://id.loc.gov/vocabulary/relators/asn
# Group metadata (label) for subproperties declaring `group: <key>`.
groups:
identity:
label: Identity
view:
render_as: compound
html_dl: true
# Subproperties declare `available_on: { properties: [<parent>] }` (no
# `class:`/`range`/`display_label`): they inherit their parent compound's class
# scope and are folded into it by the schema loader. Solr fields are derived
# per sub-property (`<parent>_<name>_<suffix>`) unless an explicit `indexing:`
# overrides or `indexing: false` opts out. (`participants` gets its `title`
# from the reused `shared_title` below.)
participant_name:
type: string
name: name
available_on:
properties:
- participants
group: identity
required: true
form:
cols: 4
participant_role:
type: controlled
name: role
available_on:
properties:
- participants
group: identity
required: true
values: [Author, Editor, Contributor, Creator, Data Collector, Funder, Publisher, Other]
form:
cols: 4
identifiers:
available_on:
class:
- GenericWork
- CollectionResource
data_type: array
type: hash
range: http://www.w3.org/2001/XMLSchema#string
display_label:
default: Identifiers
form:
primary: false
property_uri: http://purl.org/dc/terms/identifier
view:
render_as: compound
html_dl: true
identifier_value:
type: string
name: value
available_on:
properties:
- identifiers
required: true
form:
cols: 6
identifier_type:
type: controlled
name: type
available_on:
properties:
- identifiers
required: true
values: [DOI, Handle, ISBN, ISSN, URL, URN, ARK, Other]
form:
cols: 6
compound_rights:
available_on:
class:
- GenericWork
- CollectionResource
data_type: array
type: hash
range: http://www.w3.org/2001/XMLSchema#string
display_label:
default: Rights
form:
primary: false
property_uri: http://purl.org/dc/terms/rights
view:
render_as: compound
html_dl: true
compound_rights_statement:
type: controlled
name: statement
available_on:
properties:
- compound_rights
authority: rights_statements
form:
cols: 6
compound_rights_license:
type: controlled
name: license
available_on:
properties:
- compound_rights
authority: licenses
form:
cols: 6
# Overriding the derived index keys: a `string` would derive both
# `compound_rights_notes_sim` (facet) and `_tesim` (full-text). A free-text
# notes field is worth searching but not faceting, so an explicit `indexing:`
# narrows it to `_tesim` only. An explicit list replaces the derived set.
compound_rights_notes:
type: string
name: notes
available_on:
properties:
- compound_rights
indexing: [compound_rights_notes_tesim]
form:
cols: 12
relationships:
available_on:
class:
- GenericWork
- CollectionResource
data_type: array
type: hash
range: http://www.w3.org/2001/XMLSchema#string
display_label:
default: Relationships
form:
primary: true
property_uri: http://purl.org/dc/terms/relation
view:
render_as: compound
html_dl: true
display: card
relationship_item:
type: work_or_url
name: item
available_on:
properties:
- relationships
required: true
form:
cols: 6
relationship_type:
type: controlled
name: type
available_on:
properties:
- relationships
required: true
values: [References, Is Referenced By, Is Part Of, Has Part, Is Version Of, Replaces, Requires, Other]
form:
cols: 6
# A single subproperty definition reused by two compounds. The top-level key
# (`shared_title`) is globally unique, while `name: title` sets the cleaner key
# inside each compound row (so both `participants` and `relationships` rows
# carry a `title`), and `available_on.properties` lists both parents. Its Solr
# fields are derived per parent (`participants_title_sim`/`_tesim` and
# `relationships_title_sim`/`_tesim`), so the reused field is searchable in
# each compound with no collision and no hand-written `indexing:`.
shared_title:
type: string
name: title
available_on:
properties:
- participants
- relationships
form:
cols: 12
1 change: 1 addition & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ Rails/OutputSafety:
- 'app/presenters/hyrax/fixity_status_presenter.rb'
- 'app/presenters/hyrax/presents_attributes.rb'
- 'app/renderers/hyrax/renderers/attribute_renderer.rb'
- 'app/renderers/hyrax/renderers/compound_attribute_renderer.rb'
- 'spec/views/hyrax/my/works/_list_works.html.erb_spec.rb'

RSpec/DescribeClass:
Expand Down
1 change: 1 addition & 0 deletions app/assets/javascripts/hyrax.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
//= require hyrax/single_use_links_manager
//= require hyrax/copy_permalink_button
//= require hyrax/redirects
//= require hyrax/compound_metadata
//= require hyrax/dashboard_actions
//= require hyrax/batch
//= require hyrax/flot_stats
Expand Down
98 changes: 98 additions & 0 deletions app/assets/javascripts/hyrax/compound_metadata.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Behavior for compound metadata fields on resource edit forms (add/remove row,
// work_or_url select2). Event-delegated so dynamically-added rows need no
// 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() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Move the invocation into the parens that contain the function wrap-iife

if (document.hyraxCompoundsBound) return;
document.hyraxCompoundsBound = true;

// Bind select2 to a `work_or_url` sub-property. The v3 API (Hyrax bundles
// select2-rails 3.x) binds to a hidden input; createSearchChoice lets a
// typed external URL be selected as-is.
function bindWorkOrUrlInputs(root) {
if (typeof jQuery === 'undefined' || !jQuery.fn.select2) return;
jQuery(root).find('[data-hyrax-compound-work-input]').each(function() {
var $el = jQuery(this);
if ($el.hasClass('select2-offscreen') || $el.data('select2')) return; // already bound
$el.select2({
width: '100%',
allowClear: true,
placeholder: 'Search for a work or enter a URL',
minimumInputLength: 2,
// Let a typed value (e.g. an external URL) be selected as-is.
createSearchChoice: function(term) {
return { id: term, text: term };
},
// Render the current value's label (work title or the URL) on load.
initSelection: function(element, callback) {
var val = element.val();
if (!val) return;
callback({ id: val, text: element.data('label') || val });
},
ajax: {
url: $el.data('autocomplete-url'),
dataType: 'json',
quietMillis: 250,
data: function(term, page) { return { q: term }; },
results: function(data, page) {
return {
results: data.map(function(obj) {
return { id: obj.id, text: [].concat(obj.label)[0] };
})
};
}
}
});
});
}

// Bind saved rows once the DOM is ready (at script-eval time the form
// inputs don't exist yet, so the select2 would never attach). Covers both a
// fresh load and Turbolinks navigation.
function bindAll() { bindWorkOrUrlInputs(document); }
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', bindAll);
} else {
bindAll();
}
document.addEventListener('turbolinks:load', bindAll);

document.addEventListener('click', function(event) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Function has a complexity of 11 complexity

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;
}

var addButton = event.target.closest('[data-hyrax-compound-add-row]');
if (!addButton) return;
var section = addButton.closest('[data-hyrax-compound]');
if (!section) return;
var template = section.querySelector('[data-hyrax-compound-row-template]');
var rowsHost = section.querySelector('[data-hyrax-compound-rows]');
if (!template || !rowsHost) return;

// 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);
});
})();
7 changes: 7 additions & 0 deletions app/assets/stylesheets/hyrax/_collections.scss
Original file line number Diff line number Diff line change
Expand Up @@ -610,11 +610,18 @@ button.branding-banner-remove:hover {

dt {
flex-basis: 115px;
// Hold the label column fixed so every value column (dd) starts at the
// same x; otherwise a long unbreakable value (e.g. a URL link) shrinks
// this column and shifts the value left.
flex-shrink: 0;
font-weight: bold;
}

dd {
flex-basis: auto;
// Let the value column shrink to its flex space so long URLs wrap within
// it rather than forcing the row wider.
min-width: 0;
}

}
Expand Down
50 changes: 50 additions & 0 deletions app/assets/stylesheets/hyrax/_compound_metadata.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Compound metadata display on work and collection show pages.
// See documentation/compound_fields.md.
//
// `!important` throughout: the collection metadata area styles nested divs with
// a descendant selector (`.hyc-metadata div { display: flex; border-bottom }`
// 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 {
display: block !important;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

!important should not be used
Properties should be ordered border, display, flex-direction, max-width, padding

flex-direction: column;
border: 0 !important;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

!important should not be used

padding: 0 !important;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

!important should not be used

max-width: 100%;

// 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.
.hyrax-compound-entry,
.hyrax-compound-subproperty {
display: block !important;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

!important should not be used

}

.hyrax-compound-entry,
.hyrax-compound-subproperty,
.hyrax-compound-subproperty-label,
.hyrax-compound-subproperty-value {
border: 0 !important;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

!important should not be used
Properties should be ordered background, border, margin, padding

padding: 0 !important;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

!important should not be used

margin: 0;
background: none;
}

// A light divider between entries.
.hyrax-compound-entry + .hyrax-compound-entry {
margin-top: 0.5rem;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Properties should be ordered border-top, margin-top, padding-top

padding-top: 0.5rem !important;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

!important should not be used

border-top: 1px solid $gray-200 !important;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

!important should not be used

}

.hyrax-compound-subproperty {
display: block;
line-height: 1.4;
overflow-wrap: break-word;
word-break: break-word;
}

.hyrax-compound-subproperty-label {
font-weight: 600;
margin-right: 0.25rem;
}
}
2 changes: 1 addition & 1 deletion app/assets/stylesheets/hyrax/_hyrax.scss
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"hyrax/select_work_type", "hyrax/users", "hyrax/dashboard", "hyrax/sidebar",
"hyrax/controlled_vocabulary", "hyrax/accessibility", "hyrax/recent",
"hyrax/viewer", "hyrax/breadcrumbs", "hyrax/facets", "hyrax/card",
"hyrax/badge";
"hyrax/badge", "hyrax/compound_metadata";
@import "hydra-editor/multi_value_fields";
@import "typeahead";
@import "sharing_buttons";
Expand Down
Loading
Loading