Previously, this would assume a given type, and likely raise NoMethodError if the type was not what was expected. If there is a typecast error when rendering the validation failure template, display a simple error page. This should only happen if a user monkeys around with a form parameter before submitting the request. This uses a new feature in the Roda typecast_params plugin, so update to the latest Roda commit.
115 lines
5.1 KiB
Plaintext
115 lines
5.1 KiB
Plaintext
<%# locals: (action:, form_elements:, option_tree:, option_parents:, method: "POST", pre_selected_values: {}, mode: "create") %>
|
|
<% nonce = SecureRandom.base64(32)
|
|
content_security_policy.add_script_src [:nonce, nonce] %>
|
|
|
|
<script nonce="<%==nonce%>">
|
|
let option_tree = <%==JSON.generate(option_tree)%>
|
|
let option_children = <%==JSON.generate(option_parents.each_with_object(Hash.new { |h, k| h[k] = [] }) { |(child, parents), h| h[parents.last] << child if parents.any? })%>
|
|
let option_dirty = <%==JSON.generate(form_elements.map { it[:name] }.each_with_object(Hash.new { |h, k| h[k] = [] }) { |option, h| h[option] = flash.dig("old", option) || typecast_body_params.str(option) || pre_selected_values[option] })%>
|
|
</script>
|
|
|
|
<div>
|
|
<div class="grid gap-6">
|
|
<form action="<%= action %>" method="POST" id="creation-form" class="<%= method %>">
|
|
<%== csrf_tag(action, method) %>
|
|
<% container_class = (mode == "create") ? "overflow-hidden rounded-lg shadow ring-1 ring-black ring-opacity-5 bg-white" : "" %>
|
|
<div class="<%= container_class %>">
|
|
<div class="px-4 py-5 sm:p-6">
|
|
<div class="space-y-12">
|
|
<div>
|
|
<div class="mt-2 grid grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
|
|
<% selected_options = {}
|
|
pre_selected_values.each { |k, v| selected_options[k] = v.to_s }
|
|
|
|
form_elements.each do |element|
|
|
container_opening_tag = element[:opening_tag] || "<div class='col-span-full'>"
|
|
container_closing_tag = element[:closing_tag] || "</div>"
|
|
|
|
name, type, label, required, placeholder, content_generator = element.values_at(:name, :type, :label, :required, :placeholder, :content_generator)
|
|
|
|
has_option = ["radio_small_cards", "select", "checkbox"].include?(type)
|
|
options = OptionTreeGenerator.generate_allowed_options(name, option_tree, option_parents).map do |option|
|
|
opt_val = if option[name].is_a?(Sequel::Model)
|
|
option[name].ubid
|
|
elsif type == "select"
|
|
option[name][:value]
|
|
else
|
|
option[name]
|
|
end
|
|
|
|
opt_text = content_generator.call(*option.values)
|
|
|
|
parents = option.reject { |k, v| k == name }.transform_values { |v| v.is_a?(Sequel::Model) ? v.ubid : v }
|
|
|
|
parents_selected = parents.all? { |k, v| selected_options[k] == v }
|
|
submitted_value = flash.dig("old", name) || typecast_body_params.str(name)
|
|
selected_in_form_submission = submitted_value == opt_val
|
|
selected_in_pre_selection = submitted_value.nil? && selected_options[name] == opt_val
|
|
first_of_its_kind = submitted_value.nil? && selected_options[name].nil? unless type == "checkbox" && !request.get?
|
|
checked = parents_selected && (selected_in_form_submission || selected_in_pre_selection || first_of_its_kind)
|
|
selected_options[name] = opt_val if checked
|
|
|
|
opt_classes = parents.map { |k, v| "form_#{k} form_#{k}_#{v.tr(".", "-")}" }
|
|
opt_classes << "hidden" unless parents_selected
|
|
opt_classes = opt_classes.join(" ")
|
|
|
|
opt_attrs = {}
|
|
opt_attrs[:disabled] = "disabled" unless parents_selected
|
|
opt_attrs[:checked] = checked
|
|
|
|
[opt_val, opt_text, opt_classes, opt_attrs]
|
|
end if has_option
|
|
|
|
attributes = {required:}
|
|
|
|
case type
|
|
when "radio_small_cards"
|
|
locals = {name:, label:, options:, attributes:}
|
|
when "select"
|
|
locals = {name:, label:, options:, placeholder:, attributes:, description_html: element[:description_html]}
|
|
when "checkbox"
|
|
locals = {name:, label:, options:, description: element[:description]}
|
|
when "text"
|
|
locals = {name:, label:, value: element[:value], attributes: attributes.merge!({placeholder:})}
|
|
when "number"
|
|
locals = {name:, label:, type: "number", attributes: attributes.merge!({placeholder:})}
|
|
type = "text"
|
|
when "textarea"
|
|
locals = {name:, label:, description: element[:description], attributes: attributes.merge!({placeholder:, rows: 3})}
|
|
when "hidden"
|
|
selected_options[name] = element[:value]
|
|
locals = {name:, value: element[:value]}
|
|
container_opening_tag, container_closing_tag = "", ""
|
|
when "partnership_notice"
|
|
description, tos_text = content_generator.call(@flavor)
|
|
locals = {description:, tos_text:}
|
|
when "section"
|
|
locals = {label:, content: element[:content], separator: element[:separator]}
|
|
# :nocov:
|
|
else
|
|
raise "Unknown element type: #{type}"
|
|
# :nocov:
|
|
end %>
|
|
<%== container_opening_tag %>
|
|
<%== part("components/form/#{type}", **locals) %>
|
|
<%== container_closing_tag %>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="px-4 py-5 sm:p-6">
|
|
<div class="flex items-center justify-end gap-x-6">
|
|
<% if mode == "create" %>
|
|
<a href="<%= action %>" class="text-sm font-semibold leading-6 text-gray-900">Cancel</a>
|
|
<%== part("components/form/submit_button", text: "Create") %>
|
|
<% else %>
|
|
<%== part("components/form/submit_button", text: "Update") %>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|