From b302538e658015a8ad348d360fb54f5fb20cd89d Mon Sep 17 00:00:00 2001 From: Mark Bussey Date: Sun, 14 Apr 2024 20:20:45 -0500 Subject: [PATCH] Add collection selection UI to edit forms 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. --- app/assets/stylesheets/admin/items.scss | 46 +++++++++++++++---- app/helpers/t3_form_builder.rb | 4 +- app/models/field.rb | 13 ++++-- app/models/item.rb | 8 ++++ app/views/admin/items/_form.html.erb | 9 ++-- spec/helpers/t3_form_builder_spec.rb | 8 +++- spec/models/field_spec.rb | 9 ++++ spec/models/item_spec.rb | 16 ++++++- .../admin/collections/edit.html.erb_spec.rb | 28 +++++++++++ 9 files changed, 119 insertions(+), 22 deletions(-) diff --git a/app/assets/stylesheets/admin/items.scss b/app/assets/stylesheets/admin/items.scss index 2e04f087..206f7c55 100644 --- a/app/assets/stylesheets/admin/items.scss +++ b/app/assets/stylesheets/admin/items.scss @@ -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; + } } + + diff --git a/app/helpers/t3_form_builder.rb b/app/helpers/t3_form_builder.rb index 8b5d6073..e9425ddb 100644 --- a/app/helpers/t3_form_builder.rb +++ b/app/helpers/t3_form_builder.rb @@ -1,7 +1,7 @@ # 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) @@ -9,7 +9,7 @@ def vocabulary(method, options = {}) { 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 diff --git a/app/models/field.rb b/app/models/field.rb index 219017bd..9e92ca17 100644 --- a/app/models/field.rb +++ b/app/models/field.rb @@ -7,7 +7,8 @@ class Field < ApplicationRecord integer: 3, float: 4, date: 5, - boolean: 6 + boolean: 6, + vocabulary: 7 } TYPE_TO_SOLR = { @@ -16,7 +17,8 @@ class Field < ApplicationRecord 'integer' => 'lt', 'float' => 'dbt', 'date' => 'dt', - 'boolean' => 'b' + 'boolean' => 'b', + 'vocabulary' => 's' }.freeze TYPE_TO_HELPER = { @@ -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 @@ -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 diff --git a/app/models/item.rb b/app/models/item.rb index df60a726..d8e77ce9 100644 --- a/app/models/item.rb +++ b/app/models/item.rb @@ -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 @@ -115,4 +119,8 @@ def required_fields_present end end end + + def label_field + @label_field ||= blueprint.fields.first.name + end end diff --git a/app/views/admin/items/_form.html.erb b/app/views/admin/items/_form.html.erb index b7d6cc35..4f3806fe 100644 --- a/app/views/admin/items/_form.html.erb +++ b/app/views/admin/items/_form.html.erb @@ -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? %>

<%= pluralize(item.errors.count, "error") %> prohibited this item from being saved:

@@ -23,20 +23,21 @@ diff --git a/spec/helpers/t3_form_builder_spec.rb b/spec/helpers/t3_form_builder_spec.rb index ac8d091c..6e0e619f 100644 --- a/spec/helpers/t3_form_builder_spec.rb +++ b/spec/helpers/t3_form_builder_spec.rb @@ -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]') @@ -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 diff --git a/spec/models/field_spec.rb b/spec/models/field_spec.rb index 79591d88..a833c611 100644 --- a/spec/models/field_spec.rb +++ b/spec/models/field_spec.rb @@ -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 diff --git a/spec/models/item_spec.rb b/spec/models/item_spec.rb index 32cf1db6..b1e9eca3 100644 --- a/spec/models/item_spec.rb +++ b/spec/models/item_spec.rb @@ -4,7 +4,7 @@ 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' } @@ -12,7 +12,7 @@ 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' } @@ -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) } diff --git a/spec/views/admin/collections/edit.html.erb_spec.rb b/spec/views/admin/collections/edit.html.erb_spec.rb index 1455e4d8..4dc9443f 100644 --- a/spec/views/admin/collections/edit.html.erb_spec.rb +++ b/spec/views/admin/collections/edit.html.erb_spec.rb @@ -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