diff --git a/app/forms/concerns/hyrax/redirects_field_behavior.rb b/app/forms/concerns/hyrax/redirects_field_behavior.rb new file mode 100644 index 0000000000..bac11ef490 --- /dev/null +++ b/app/forms/concerns/hyrax/redirects_field_behavior.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +module Hyrax + # Form-side handling for the `redirects` nested-attribute field. + # + # Submitted form payloads arrive under `redirects_attributes` and are + # turned into plain hashes (the persisted shape — see + # `config/metadata/redirects.yaml`, `type: hash`) by the populator. + # On render, the prepopulator wraps each persisted hash in a + # `Hyrax::Redirect` value object so the view can call `.path` / + # `.canonical` / `.sequence`. + # + # The `deserialize!` override removes the renamed `redirects` key + # before Reform's `from_hash` runs, so the form's `redirects` + # property is written exclusively by the populator. See + # `Hyrax::BasedNearFieldBehavior` for the parallel pattern. + # + # ## Feature gating + # + # `self.included` runs at class load time and uses the structural + # gate `Hyrax.config.redirects_enabled?`. The runtime Flipflop + # check isn't meaningful here because the form class is being + # defined, not handling a request. + # + # All runtime-side methods (`deserialize!`, populator, prepopulator) + # delegate to `Hyrax.config.redirects_active?`, the two-gate + # combinator (`redirects_enabled? && Flipflop.redirects?`). Calling + # `Flipflop.redirects?` directly is unsafe when the config is off + # because the feature isn't registered in that case. + module RedirectsFieldBehavior + def self.included(descendant) + return unless Hyrax.config.redirects_enabled? + descendant.property :redirects_attributes, + virtual: true, + populator: :redirects_attributes_populator, + prepopulator: :redirects_attributes_prepopulator + end + + # Reform's FormBuilderMethods rewrites `redirects_attributes` → + # `redirects` before `from_hash` runs. Strip the renamed key so + # the populator on `redirects_attributes` is the single entry + # point for form-driven writes. + def deserialize!(params) + result = super + if Hyrax.config.redirects_active? && result.respond_to?(:delete) + result.delete('redirects') + result.delete(:redirects) + end + result + end + + private + + # Builds plain hashes (the persisted shape) from the submitted + # `redirects_attributes` payload. Drops rows marked for destruction + # or with a blank path. Normalizes paths to the canonical form + # stored in the uniqueness ledger. + def redirects_attributes_populator(fragment:, **_options) + return unless respond_to?(:redirects) + return unless Hyrax.config.redirects_active? + entries = Array(fragment&.values) + .reject { |row| row['_destroy'].to_s == 'true' || row['path'].to_s.strip.empty? } + .each_with_index.map do |row, i| + { 'path' => Hyrax::RedirectPathNormalizer.call(row['path']), + 'canonical' => row['canonical'].to_s == 'true', + 'sequence' => row['sequence'].presence&.to_i || i } + end + self.redirects = entries + end + + # Wraps each persisted hash in a `Hyrax::Redirect` value object for + # the form view. Mirrors how `BasedNearFieldBehavior` hydrates URI + # strings into `ControlledVocabularies::Location` instances. + def redirects_attributes_prepopulator + return unless respond_to?(:redirects) + return unless Hyrax.config.redirects_active? + self.redirects = Array(redirects).map { |entry| Hyrax::Redirect.wrap(entry) } + end + end +end diff --git a/app/forms/hyrax/forms/resource_form.rb b/app/forms/hyrax/forms/resource_form.rb index 5e243707aa..1c7b63e50d 100644 --- a/app/forms/hyrax/forms/resource_form.rb +++ b/app/forms/hyrax/forms/resource_form.rb @@ -27,6 +27,7 @@ class ResourceForm < Hyrax::ChangeSet # rubocop:disable Metrics/ClassLength end include BasedNearFieldBehavior + include RedirectsFieldBehavior class_attribute :model_class property :human_readable_type, writable: false @@ -112,9 +113,13 @@ def initialize(deprecated_resource = nil, resource: nil) class << self def inherited(subclass) - # this is a noop if based near is not defined on a given model - # we need these to be before and included properties + # Field Behaviors must be prepended onto every subclass so their + # `deserialize!` overrides land above Reform's base method on the + # ancestor chain. Each behavior gates itself internally and is a + # no-op when its feature is off or its property isn't on the + # subclass's model. subclass.prepend(BasedNearFieldBehavior) + subclass.prepend(RedirectsFieldBehavior) super end diff --git a/app/helpers/hyrax/collections_helper.rb b/app/helpers/hyrax/collections_helper.rb index 3fef59899d..09374db983 100644 --- a/app/helpers/hyrax/collections_helper.rb +++ b/app/helpers/hyrax/collections_helper.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true module Hyrax module CollectionsHelper # rubocop:disable Metrics/ModuleLength + include Hyrax::RedirectsTabHelper + ## # @since 3.1.0 # @return [Array] diff --git a/app/helpers/hyrax/redirects_tab_helper.rb b/app/helpers/hyrax/redirects_tab_helper.rb new file mode 100644 index 0000000000..28eee88544 --- /dev/null +++ b/app/helpers/hyrax/redirects_tab_helper.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Hyrax + ## + # Decides whether the Aliases tab should appear on a work or + # collection edit form. Included into `WorkFormHelper` and + # `CollectionsHelper` so both forms share one rule. + # + # The tab appears only when the redirects feature is fully active + # (`Hyrax.config.redirects_active?`), the form is a ResourceForm + # (the only form pipeline with the redirects populator, validator, + # and prepopulator wired in), AND the form's underlying resource + # carries the `redirects` attribute. The structural check guards + # against adopter work or collection classes that don't include + # the redirects schema; without it the tab would render and crash + # on `f.object.redirects`. + module RedirectsTabHelper + def redirects_tab?(form) + return false unless Hyrax.config.redirects_active? + return false unless redirects_supported_form?(form) + target = form.respond_to?(:model) ? form.model : form + target.respond_to?(:redirects) + end + + def redirects_supported_form?(form) + form.is_a?(Hyrax::Forms::ResourceForm) + end + end +end diff --git a/app/helpers/hyrax/work_form_helper.rb b/app/helpers/hyrax/work_form_helper.rb index 4ac993465a..d6f4b1faac 100644 --- a/app/helpers/hyrax/work_form_helper.rb +++ b/app/helpers/hyrax/work_form_helper.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true module Hyrax module WorkFormHelper + include Hyrax::RedirectsTabHelper + ## # @todo this implementation hits database backends (solr) and is invoked # from views. refactor to avoid @@ -30,11 +32,13 @@ def admin_set_options # @param form [Hyrax::Forms::WorkForm, Hyrax::Forms::ResourceForm] # @return [Array] the list of names of tabs to be rendered in the form def form_tabs_for(form:) - if form.instance_of? Hyrax::Forms::BatchUploadForm - %w[files metadata relationships] - else - %w[metadata files relationships] - end + tabs = if form.instance_of? Hyrax::Forms::BatchUploadForm + %w[files metadata relationships] + else + %w[metadata files relationships] + end + tabs << 'redirects' if redirects_tab?(form) + tabs end ## diff --git a/app/views/hyrax/base/_form_redirects.html.erb b/app/views/hyrax/base/_form_redirects.html.erb new file mode 100644 index 0000000000..0faa9e57b3 --- /dev/null +++ b/app/views/hyrax/base/_form_redirects.html.erb @@ -0,0 +1,52 @@ +<%# Aliases tab — see documentation/redirects.md %> +<% return unless redirects_tab?(f.object) %> +

<%= t('hyrax.works.form.redirects.heading') %>

+ +

<%= sanitize t('hyrax.works.form.redirects.help_html'), tags: %w[code] %>

+ + + + + + + + + + + <%# Wrap each row through Hyrax::Redirect.wrap so the view can rely on + the presenter API (.path, .canonical, .sequence). On initial render + the prepopulator already wrapped them; on a re-render after a + failed save, f.object.redirects holds the populator's plain hashes, + and we wrap them here. %> + <% rows = Array(f.object.redirects).map { |e| Hyrax::Redirect.wrap(e) } + [Hyrax::Redirect.new(path: nil)] %> + <% rows.each_with_index do |entry, index| %> + <% input_id = "#{f.object_name}_redirects_attributes_#{index}_path" %> + <% label_text = entry.path.present? ? t('hyrax.works.form.redirects.path_label') : t('hyrax.works.form.redirects.new_path_label') %> + + + + + <% end %> + +
<%= t('hyrax.works.form.redirects.caption') %>
<%= t('hyrax.works.form.redirects.header.path') %><%= t('hyrax.works.form.redirects.header.actions') %>
+ <%= label_tag input_id, label_text, class: 'sr-only' %> +
+
+ <%= request.base_url %> +
+ <%= text_field_tag "#{f.object_name}[redirects_attributes][#{index}][path]", + entry.path, + id: input_id, + class: 'form-control', + placeholder: t('hyrax.works.form.redirects.placeholder.path') %> +
+
+ <% if entry.path.present? %> + + <% end %> +
diff --git a/app/views/hyrax/dashboard/collections/_form.html.erb b/app/views/hyrax/dashboard/collections/_form.html.erb index 5e8c87441d..a19ee698b0 100644 --- a/app/views/hyrax/dashboard/collections/_form.html.erb +++ b/app/views/hyrax/dashboard/collections/_form.html.erb @@ -28,6 +28,13 @@ <% end %> + <% if redirects_tab?(@form) %> + + <% end %> <% end %> @@ -109,6 +116,16 @@ <% end %> + + <% if redirects_tab?(f.object) %> +
+
+
+ <%= render 'hyrax/base/form_redirects', f: f %> +
+
+
+ <% end %> <% end %>