Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
80 changes: 80 additions & 0 deletions app/forms/concerns/hyrax/redirects_field_behavior.rb
Original file line number Diff line number Diff line change
@@ -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
9 changes: 7 additions & 2 deletions app/forms/hyrax/forms/resource_form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions app/helpers/hyrax/collections_helper.rb
Original file line number Diff line number Diff line change
@@ -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<SolrDocument>]
Expand Down
29 changes: 29 additions & 0 deletions app/helpers/hyrax/redirects_tab_helper.rb
Original file line number Diff line number Diff line change
@@ -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
14 changes: 9 additions & 5 deletions app/helpers/hyrax/work_form_helper.rb
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -30,11 +32,13 @@ def admin_set_options
# @param form [Hyrax::Forms::WorkForm, Hyrax::Forms::ResourceForm]
# @return [Array<String>] 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

Comment thread
laritakr marked this conversation as resolved.
##
Expand Down
52 changes: 52 additions & 0 deletions app/views/hyrax/base/_form_redirects.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<%# Aliases tab — see documentation/redirects.md %>
<% return unless redirects_tab?(f.object) %>
<h2 class="h3 mt-4"><%= t('hyrax.works.form.redirects.heading') %></h2>

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

<table class="table table-striped">
<caption class="sr-only"><%= t('hyrax.works.form.redirects.caption') %></caption>
<thead>
<tr>
<th scope="col"><%= t('hyrax.works.form.redirects.header.path') %></th>
<th scope="col" class="sr-only"><%= t('hyrax.works.form.redirects.header.actions') %></th>
</tr>
</thead>
<tbody>
<%# 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') %>
<tr>
<td>
<%= label_tag input_id, label_text, class: 'sr-only' %>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><%= request.base_url %></span>
</div>
<%= 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') %>
Comment thread
laritakr marked this conversation as resolved.
</div>
</td>
<td class="text-right" style="width: 1%;">
<% if entry.path.present? %>
<button type="button"
class="btn close"
aria-label="<%= t('hyrax.works.form.redirects.remove_with_path_label', path: entry.path) %>"
onclick="this.closest('tr').remove()">
<span aria-hidden="true">&times;</span>
</button>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
17 changes: 17 additions & 0 deletions app/views/hyrax/dashboard/collections/_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@
</a>
</li>
<% end %>
<% if redirects_tab?(@form) %>
<li class="nav-item" role="presentation">
<a href="#redirects" role="tab" data-toggle="tab" class="nav-link nav-safety-confirm">
<%= t('.tabs.redirects') %>
</a>
</li>
<% end %>
<% end %>
</ul>

Expand Down Expand Up @@ -109,6 +116,16 @@
</div>
</div>
<% end %>

<% if redirects_tab?(f.object) %>
<div id="redirects" class="tab-pane">
<div class="card labels">
<div class="card-body">
<%= render 'hyrax/base/form_redirects', f: f %>
</div>
</div>
</div>
<% end %>
Comment thread
laritakr marked this conversation as resolved.
<% end %>

<div class="card-footer">
Expand Down
16 changes: 16 additions & 0 deletions config/locales/hyrax.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -925,6 +925,7 @@ en:
branding: Branding
description: Description
discovery: Discovery
redirects: Aliases
relationships: Relationships
sharing: Sharing
form_branding:
Expand Down Expand Up @@ -1809,9 +1810,24 @@ en:
in_collections: Collections
in_other_works: This Work in Other Works
in_this_work: Other Works in this Work
redirects:
caption: List of URL aliases for this resource
errors_heading: Please correct the following alias errors
header:
actions: Actions
path: Path
heading: URL Aliases
help_html: 'Register legacy URL paths that should redirect to this record. Each path will resolve from this site''s root. Reserved Hyrax routes (e.g. <code>/concern</code>, <code>/dashboard</code>) cannot be used.'
new_path_label: New alias path
path_label: Alias path
placeholder:
path: Enter a legacy path...
remove_label: Remove
remove_with_path_label: 'Remove %{path}'
tab:
files: Files
metadata: Descriptions
redirects: Aliases
relationships: Relationships
share: Sharing
visibility_until: until
Expand Down
Loading
Loading