Files
ubicloud/views/components/form/resource_creation_form.erb
Jeremy Evans 23601a07a3 Use correct params typecasting when handling validation failures
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.
2025-08-08 01:52:14 +09:00

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>