Skip to content

Commit

Permalink
Add collection selection UI to edit forms
Browse files Browse the repository at this point in the history
This change adds the code necessary to allow administrators to
set a field's data_type to `vocabulary` and have the edit form
display a list of availabel collections in a selection dropdown.

Right now, the `vocabulary` data_type only supports collections,
but we hope to generalize the funcionality to other controlled
vocabularies in the future.
  • Loading branch information
mark-dce committed Apr 15, 2024
1 parent 9f8ab02 commit b302538
Show file tree
Hide file tree
Showing 9 changed files with 119 additions and 22 deletions.
46 changes: 37 additions & 9 deletions app/assets/stylesheets/admin/items.scss
Original file line number Diff line number Diff line change
@@ -1,13 +1,41 @@
.item input, .item textarea {
width: 50rem;
display: block;
}
.item {
input, textarea {
width: 50rem;
display: block;
}

select {
width: 35rem;
margin-right: 2rem;
height: 1.9rem;
}

button.delete_value {
width: 13rem;
}

button.add_value {
display: block;
margin-top: 0.25rem;
}

.item .required {
font-style: italic;
.required {
font-style: italic;
}

label {
margin-bottom: 0.5rem;
}
}

#choose_blueprint button {
display: block;
margin-bottom: 0.25rem;
#choose_blueprint {
width: fit-content;

button {
display: block;
width: 100%;
margin-bottom: 0.25rem;
}
}


4 changes: 2 additions & 2 deletions app/helpers/t3_form_builder.rb
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
# Custom form controls to support T3 field-specific data types
# e.g. #vacabulary provides a customized select populated with a defined vocabulary
class T3FormBuilder < ActionView::Helpers::FormBuilder
def vocabulary(method, options = {})
def vocabulary_field(method, options = {})
multiple = options.delete(:multiple)
selected = options.delete(:value) || ''
select_options = options.except(:multiple)
.reverse_merge(
{ name: @template.field_name(@object_name, method, multiple: multiple) }
)
.merge({ selected: selected })
option_tags = @template.options_from_collection_for_select(Collection.order(:created_at), :id, :id, selected)
option_tags = @template.options_from_collection_for_select(Collection.order(:created_at), :label, :label, selected)
select(method, option_tags, { prompt: 'Select one', selected: '', disabled: true }, select_options)
end
end
13 changes: 10 additions & 3 deletions app/models/field.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ class Field < ApplicationRecord
integer: 3,
float: 4,
date: 5,
boolean: 6
boolean: 6,
vocabulary: 7
}

TYPE_TO_SOLR = {
Expand All @@ -16,7 +17,8 @@ class Field < ApplicationRecord
'integer' => 'lt',
'float' => 'dbt',
'date' => 'dt',
'boolean' => 'b'
'boolean' => 'b',
'vocabulary' => 's'
}.freeze

TYPE_TO_HELPER = {
Expand All @@ -25,7 +27,8 @@ class Field < ApplicationRecord
'integer' => :number_field,
'float' => :number_field,
'date' => :date_field,
'boolean' => :check_box
'boolean' => :check_box,
'vocabulary' => :vocabulary_field
}.freeze

validates :name, presence: true
Expand Down Expand Up @@ -98,6 +101,10 @@ def move(position) # rubocop:disable Metrics/MethodLength
end
end

def form_helper
TYPE_TO_HELPER[data_type]
end

private

def clear_solr_field
Expand Down
8 changes: 8 additions & 0 deletions app/models/item.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ def prune_orphans(batch, previously_checked)
end
end

def label
metadata[label_field]
end

def to_partial_path
"admin/#{super}"
end
Expand Down Expand Up @@ -115,4 +119,8 @@ def required_fields_present
end
end
end

def label_field
@label_field ||= blueprint.fields.first.name
end
end
9 changes: 5 additions & 4 deletions app/views/admin/items/_form.html.erb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<%= form_with(model: item, scope: 'item', id: 'item_fields', class: 'item') do |form| %>
<%= form_with(model: item, scope: 'item', id: 'item_fields', class: 'item', builder: T3FormBuilder) do |form| %>
<% if item.errors.any? %>
<div style="color: red">
<h2><%= pluralize(item.errors.count, "error") %> prohibited this item from being saved:</h2>
Expand All @@ -23,20 +23,21 @@
<label>
<%= field.name -%>
<%= content_tag(:span, '(required)', class: 'required') if field.required -%>
<br/>

<% field_method = Field::TYPE_TO_HELPER[field.data_type] %>
<% if field.multiple %>
<% item_detail.object[field.name] ||= [nil] %>
<% item_detail.object[field.name].each.with_index(1) do |value, index| %>
<%= item_detail.send(field_method, field.name,
<%= item_detail.send(field.form_helper, field.name,
value: value, id: item_detail.field_id(field.name, index),
multiple: field.multiple, aria: {label: field.name + " #{index}",
required: field.required}) %>
<%= form.button t('t3.item.delete_entry', field: field.name, index: index), name: 'refresh', value: ['delete', field.name, index], class: 'delete_value' %>
<br/>
<% end %>
<%= form.button t('t3.item.add_entry', field: field.name), name: 'refresh', value: ['add', field.name, -1], class: 'add_value' %>
<% else %>
<%= item_detail.send(field_method, field.name, multiple: field.multiple,
<%= item_detail.send(field.form_helper, field.name, multiple: field.multiple,
aria: {label: field.name, required: field.required}) %>
<% end %>
</label>
Expand Down
8 changes: 6 additions & 2 deletions spec/helpers/t3_form_builder_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
let(:tag_options) { {} }

describe '#vocabulary' do
let(:vocabulary_helper) { Capybara.string(form_builder.vocabulary(:collection, tag_options)) }
let(:vocabulary_helper) { Capybara.string(form_builder.vocabulary_field(:collection, tag_options)) }

it 'renders a select with the expected id' do
expect(vocabulary_helper).to have_select('item[metadata][collection]')
Expand All @@ -33,7 +33,11 @@
let(:tag_options) { { value: 'Green' } }

before do
collections = [OpenStruct.new({ id: 'Red' }), OpenStruct.new({ id: 'Green' }), OpenStruct.new({ id: 'Blue' })]
collections = [
instance_double(Collection, { label: 'Red', id: 5 }),
instance_double(Collection, { label: 'Green', id: 20 }),
instance_double(Collection, { label: 'Blue', id: 25 })
]
allow(Collection).to receive(:order).and_return(collections)
end

Expand Down
9 changes: 9 additions & 0 deletions spec/models/field_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,15 @@
end
end

describe '#form_helper' do
it 'is defined for each data_type' do
described_class.data_types.each_key do |data_type|
field.data_type = data_type
expect(field.form_helper).to be_present, "Missing form_helper mapping for data_type: #{data_type}"
end
end
end

describe '#solr_suffix' do
it 'ends in "m" for multivalued fields' do
field.multiple = true
Expand Down
16 changes: 14 additions & 2 deletions spec/models/item_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
let(:new_item) { described_class.new(metadata: basic_description) }
let(:basic_description) do
{
'Title' => 'One Hundred Years of Solitute',
'Title' => 'One Hundred Years of Solitude',
'Author' => ['Márquez, Gabriel García'],
'Date' => '1967'
}
end
let(:solr_doc) do
{
'blueprint_ssi' => 'Sample Blueprint',
'title_tesi' => 'One Hundred Years of Solitute',
'title_tesi' => 'One Hundred Years of Solitude',
'author_tesim' => ['Márquez, Gabriel García'],
'date_ltsi' => '1967'
}
Expand Down Expand Up @@ -46,6 +46,18 @@
end
end

describe '#label' do
let(:blueprint) { FactoryBot.build(:blueprint, name: 'Sample Blueprint') }
let(:new_item) { described_class.new(blueprint: blueprint, metadata: basic_description.as_json) }

it 'returns the value of the bluprint title field' do
allow(blueprint).to receive(:fields).and_return(
[FactoryBot.build(:field, name: 'Title', data_type: 'text_en')]
)
expect(new_item.label).to eq 'One Hundred Years of Solitude'
end
end

describe '#to_solr' do
let(:blueprint) { FactoryBot.build(:blueprint, name: 'Sample Blueprint') }
let(:new_item) { described_class.new(blueprint: blueprint, metadata: basic_description.as_json) }
Expand Down
28 changes: 28 additions & 0 deletions spec/views/admin/collections/edit.html.erb_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -168,4 +168,32 @@
expect(rendered).to have_button('refresh', value: 'delete keyword 2')
end
end

describe 'a vocabulary field' do
let(:vocabulary_field) do
FactoryBot.build(:field, name: 'collection', data_type: 'vocabulary', multiple: false, id: 1, sequence: 1)
end

before do
collections = [
instance_double(Collection, { id: 1, label: 'Cyan' }),
instance_double(Collection, { id: 2, label: 'Magenta' }),
instance_double(Collection, { id: 4, label: 'Yellow' })
]
allow(Collection).to receive(:order).and_return(collections)
end

it 'renders a slection list' do
allow(blueprint).to receive(:fields).and_return([vocabulary_field])
render
expect(rendered).to have_select('item[metadata][collection]')
end

it 'lists available values' do
allow(blueprint).to receive(:fields).and_return([vocabulary_field])
render
select_options = Capybara.string(rendered).all('select option').map(&:text)
expect(select_options).to include('Cyan', 'Magenta', 'Yellow')
end
end
end

0 comments on commit b302538

Please sign in to comment.