Skip to content

Commit 385b205

Browse files
maebealeclaude
andcommitted
Add one-time field hiding, flexbox layout, and cocoon insertion fix
- Add `one_time` boolean to form_fields for cross-form answer hiding - Two-tier answer hiding: one-time checks all forms, regular checks within event - Switch field rows to flexbox with wrap for responsive layout - Make section header names editable text fields - New cocoon fields now append to bottom of form field list - Add ONE_TIME_GROUPS to FormBuilderService for professional/background sections Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent f44d661 commit 385b205

7 files changed

Lines changed: 95 additions & 62 deletions

File tree

app/controllers/events/public_registrations_controller.rb

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -142,25 +142,47 @@ def visible_form_fields
142142
end
143143

144144
if @form.hide_answered_form_questions?
145-
existing_submission = @form.form_submissions.find_by(person: person)
146-
if existing_submission
147-
answered_field_ids = existing_submission.form_answers
148-
.joins(:form_field)
149-
.where(form_fields: { visibility: :answers_on_file })
150-
.where.not(text: [ nil, "" ])
151-
.pluck(:form_field_id)
152-
if answered_field_ids.any?
153-
scope = scope.where.not(id: answered_field_ids)
154-
155-
# Hide section headers when all their non-header fields are answered
156-
answered_groups = @form.form_fields.where(id: answered_field_ids)
157-
.pluck(:field_group).uniq.compact
158-
answered_groups.each do |group|
159-
group_field_ids = @form.form_fields.where(field_group: group, visibility: :answers_on_file)
160-
.where.not(answer_type: :group_header).ids
161-
if group_field_ids.any? && (group_field_ids - answered_field_ids).empty?
162-
scope = scope.where.not(field_group: group, answer_type: :group_header, visibility: :answers_on_file)
163-
end
145+
answered_field_ids = []
146+
147+
# One-time fields: hide if answered on ANY form submission for this person
148+
one_time_field_ids = @form.form_fields.where(visibility: :answers_on_file, one_time: true)
149+
.where.not(answer_type: :group_header).ids
150+
if one_time_field_ids.any?
151+
answered_one_time = FormAnswer.joins(:form_submission)
152+
.where(form_submissions: { person_id: person.id })
153+
.where(form_field_id: one_time_field_ids)
154+
.where.not(text: [ nil, "" ])
155+
.pluck(:form_field_id)
156+
answered_field_ids.concat(answered_one_time)
157+
end
158+
159+
# Regular fields: hide if answered on forms within this event
160+
event_form_ids = @event.forms.ids
161+
event_submissions = FormSubmission.where(person: person, form_id: event_form_ids)
162+
if event_submissions.exists?
163+
regular_field_ids = @form.form_fields.where(visibility: :answers_on_file, one_time: false)
164+
.where.not(answer_type: :group_header).ids
165+
if regular_field_ids.any?
166+
answered_regular = FormAnswer.where(form_submission: event_submissions)
167+
.where(form_field_id: regular_field_ids)
168+
.where.not(text: [ nil, "" ])
169+
.pluck(:form_field_id)
170+
answered_field_ids.concat(answered_regular)
171+
end
172+
end
173+
174+
answered_field_ids.uniq!
175+
if answered_field_ids.any?
176+
scope = scope.where.not(id: answered_field_ids)
177+
178+
# Hide section headers when all their non-header fields are answered
179+
answered_groups = @form.form_fields.where(id: answered_field_ids)
180+
.pluck(:field_group).uniq.compact
181+
answered_groups.each do |group|
182+
group_field_ids = @form.form_fields.where(field_group: group, visibility: :answers_on_file)
183+
.where.not(answer_type: :group_header).ids
184+
if group_field_ids.any? && (group_field_ids - answered_field_ids).empty?
185+
scope = scope.where.not(field_group: group, answer_type: :group_header, visibility: :answers_on_file)
164186
end
165187
end
166188
end

app/controllers/forms_controller.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ def form_params
119119
:name, :hide_answered_person_questions, :hide_answered_form_questions,
120120
form_fields_attributes: [
121121
:id, :question, :answer_type, :is_required, :instructional_hint,
122-
:field_key, :field_group, :position, :visibility, :_destroy
122+
:field_key, :field_group, :position, :visibility, :one_time, :_destroy
123123
]
124124
)
125125
end

app/services/form_builder_service.rb

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,9 @@ def self.update_sections!(form, new_sections)
110110
"post_event_feedback" => :answers_on_file
111111
}.freeze
112112

113+
# Groups where answers carry across all events (ask once ever)
114+
ONE_TIME_GROUPS = %w[professional background].freeze
115+
113116
def add_header(form, position, title, group:)
114117
position += 1
115118
form.form_fields.create!(
@@ -120,7 +123,8 @@ def add_header(form, position, title, group:)
120123
is_required: false,
121124
field_key: nil,
122125
field_group: group,
123-
visibility: GROUP_VISIBILITY.fetch(group, :always_ask)
126+
visibility: GROUP_VISIBILITY.fetch(group, :always_ask),
127+
one_time: ONE_TIME_GROUPS.include?(group)
124128
)
125129
position
126130
end
@@ -137,7 +141,8 @@ def add_field(form, position, question, answer_type, key:, group:, required: tru
137141
instructional_hint: hint,
138142
field_key: key,
139143
field_group: group,
140-
visibility: GROUP_VISIBILITY.fetch(group, :always_ask)
144+
visibility: GROUP_VISIBILITY.fetch(group, :always_ask),
145+
one_time: ONE_TIME_GROUPS.include?(group)
141146
)
142147

143148
if options.present?

app/views/forms/_form_field_fields.html.erb

Lines changed: 32 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -32,49 +32,44 @@
3232
{},
3333
class: "text-xs rounded-full border px-2 py-0.5 cursor-pointer",
3434
data: { controller: "chip-select", chip_select_styles_value: visibility_styles, action: "change->chip-select#update" } %>
35-
<span class="text-lg font-semibold text-gray-800"><%= field.question %></span>
36-
<%= f.hidden_field :question %>
35+
<%= f.text_field :question, class: "flex-1 text-lg font-semibold text-gray-800 rounded border-gray-300 shadow-sm px-2 py-1" %>
3736
<div class="ml-auto">
3837
<%= link_to_remove_association "Remove", f,
3938
class: "text-sm text-gray-400 hover:text-red-600 underline" %>
4039
</div>
4140
</div>
4241
<% else %>
43-
<div class="flex-1 grid grid-cols-12 gap-3 items-center">
44-
<div class="col-span-5 flex items-center gap-2">
45-
<%= f.select :visibility, visibility_options,
46-
{},
47-
class: "text-xs rounded-full border px-2 py-0.5 shrink-0 cursor-pointer",
48-
data: { controller: "chip-select", chip_select_styles_value: visibility_styles, action: "change->chip-select#update" } %>
49-
<%= f.text_field :question, class: "w-full rounded border-gray-300 shadow-sm px-2 py-1 text-sm" %>
50-
</div>
51-
<div class="col-span-3">
52-
<%
53-
type_order = %w[free_form_input_one_line free_form_input_paragraph multiple_choice_radio multiple_choice_checkbox no_user_input group_header]
54-
type_labels = {
55-
"group_header" => "Section header",
56-
"free_form_input_one_line" => "One line",
57-
"free_form_input_paragraph" => "Paragraph",
58-
"multiple_choice_radio" => "Multiple choice radio",
59-
"multiple_choice_checkbox" => "Multiple choice checkbox",
60-
"no_user_input" => "Informational-only"
61-
}
62-
type_options = type_order.map { |t| [ type_labels[t] || t.titleize.gsub("_", " "), t ] }
63-
%>
64-
<%= f.select :answer_type, type_options,
65-
{},
66-
class: "w-full rounded border-gray-300 shadow-sm px-2 py-1 text-sm" %>
67-
</div>
68-
<div class="col-span-2">
69-
<label class="flex items-center gap-1 text-sm text-gray-600">
70-
<%= f.check_box :is_required, class: "rounded border-gray-300 text-blue-600" %>
71-
Required
72-
</label>
73-
</div>
74-
<div class="col-span-2 text-right">
75-
<%= link_to_remove_association "Remove", f,
76-
class: "text-sm text-gray-400 hover:text-red-600 underline" %>
77-
</div>
42+
<div class="flex-1 flex flex-wrap items-center gap-2">
43+
<%= f.select :visibility, visibility_options,
44+
{},
45+
class: "text-xs rounded-full border px-2 py-0.5 shrink-0 cursor-pointer",
46+
data: { controller: "chip-select", chip_select_styles_value: visibility_styles, action: "change->chip-select#update" } %>
47+
<%= f.text_field :question, class: "min-w-0 flex-[3_1_10rem] rounded border-gray-300 shadow-sm px-2 py-1 text-sm" %>
48+
<%
49+
type_order = %w[free_form_input_one_line free_form_input_paragraph multiple_choice_radio multiple_choice_checkbox no_user_input group_header]
50+
type_labels = {
51+
"group_header" => "Section header",
52+
"free_form_input_one_line" => "One line",
53+
"free_form_input_paragraph" => "Paragraph",
54+
"multiple_choice_radio" => "Multiple choice radio",
55+
"multiple_choice_checkbox" => "Multiple choice checkbox",
56+
"no_user_input" => "Informational-only"
57+
}
58+
type_options = type_order.map { |t| [ type_labels[t] || t.titleize.gsub("_", " "), t ] }
59+
%>
60+
<%= f.select :answer_type, type_options,
61+
{},
62+
class: "flex-[2_1_11rem] rounded border-gray-300 shadow-sm px-2 py-1 text-sm" %>
63+
<label class="flex items-center gap-1 text-sm text-gray-600 shrink-0">
64+
<%= f.check_box :is_required, class: "rounded border-gray-300 text-blue-600" %>
65+
Required
66+
</label>
67+
<label class="flex items-center gap-1 text-sm text-gray-600 shrink-0" title="Hide if already answered on any form (not just this event)">
68+
<%= f.check_box :one_time, class: "rounded border-gray-300 text-amber-600" %>
69+
One-time
70+
</label>
71+
<%= link_to_remove_association "Remove", f,
72+
class: "text-sm text-gray-400 hover:text-red-600 underline shrink-0 ml-auto" %>
7873
</div>
7974
<% end %>
8075
</div>

app/views/forms/edit.html.erb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@
6161
</div>
6262
<div class="flex items-center gap-1.5">
6363
<span class="text-xs px-1.5 py-0.5 rounded-full bg-amber-100 text-amber-700">Answers on file</span>
64-
<span class="text-xs text-gray-500">Hidden when info is already known</span>
64+
<span class="text-xs text-gray-500">Hidden when answered on this event's forms; if "One-time", hidden when answered on any form</span>
6565
</div>
6666
</div>
6767
</div>
@@ -74,6 +74,8 @@
7474

7575
<div class="px-6 py-3 border-t border-gray-100">
7676
<%= link_to_add_association "+ Add field", f, :form_fields,
77+
data: { association_insertion_node: "[data-controller='form-fields-sortable']",
78+
association_insertion_method: "append" },
7779
class: "text-sm text-blue-600 hover:text-blue-800 font-medium" %>
7880
</div>
7981
</div>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
class AddOneTimeToFormFields < ActiveRecord::Migration[8.0]
2+
def change
3+
add_column :form_fields, :one_time, :boolean, default: false, null: false
4+
end
5+
end

db/schema.rb

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
#
1111
# It's strongly recommended that you check this file into your version control system.
1212

13-
ActiveRecord::Schema[8.1].define(version: 2026_03_09_120000) do
13+
ActiveRecord::Schema[8.1].define(version: 2026_03_09_140000) do
1414
create_table "action_text_mentions", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
1515
t.bigint "action_text_rich_text_id", null: false
1616
t.datetime "created_at", null: false
@@ -530,6 +530,7 @@
530530
t.integer "form_id", null: false
531531
t.text "instructional_hint"
532532
t.boolean "is_required", default: true
533+
t.boolean "one_time", default: false, null: false
533534
t.integer "parent_id"
534535
t.integer "position"
535536
t.string "question", null: false
@@ -628,6 +629,9 @@
628629
t.text "email_body_html"
629630
t.text "email_body_text"
630631
t.text "email_subject"
632+
t.datetime "error_at"
633+
t.string "error_class"
634+
t.text "error_message"
631635
t.string "kind", null: false
632636
t.integer "noticeable_id"
633637
t.string "noticeable_type"
@@ -835,7 +839,7 @@
835839
t.string "type"
836840
t.datetime "updated_at", precision: nil, null: false
837841
t.integer "windows_type_id", null: false
838-
t.integer "workshop_id", null: false
842+
t.integer "workshop_id"
839843
t.string "workshop_name"
840844
t.index ["created_by_id"], name: "index_reports_on_created_by_id"
841845
t.index ["organization_id"], name: "index_reports_on_organization_id"

0 commit comments

Comments
 (0)