formblocks 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/AGENTS.md +57 -0
- data/CHANGELOG.md +24 -0
- data/MIT-LICENSE +20 -0
- data/README.md +472 -0
- data/Rakefile +24 -0
- data/app/controllers/concerns/formblocks/request_context.rb +58 -0
- data/app/controllers/formblocks/application_controller.rb +16 -0
- data/app/controllers/formblocks/assets_controller.rb +27 -0
- data/app/controllers/formblocks/blocks_controller.rb +81 -0
- data/app/controllers/formblocks/dashboard_controller.rb +41 -0
- data/app/controllers/formblocks/forms_controller.rb +71 -0
- data/app/controllers/formblocks/pages_controller.rb +44 -0
- data/app/controllers/formblocks/public/forms_controller.rb +74 -0
- data/app/controllers/formblocks/responses_controller.rb +39 -0
- data/app/controllers/formblocks/settings_controller.rb +30 -0
- data/app/helpers/formblocks/application_helper.rb +93 -0
- data/app/models/concerns/formblocks/attachable.rb +43 -0
- data/app/models/formblocks/application_record.rb +7 -0
- data/app/models/formblocks/block.rb +177 -0
- data/app/models/formblocks/blocks/checkbox.rb +22 -0
- data/app/models/formblocks/blocks/email.rb +23 -0
- data/app/models/formblocks/blocks/heading.rb +13 -0
- data/app/models/formblocks/blocks/hidden.rb +25 -0
- data/app/models/formblocks/blocks/image.rb +14 -0
- data/app/models/formblocks/blocks/input.rb +59 -0
- data/app/models/formblocks/blocks/name.rb +9 -0
- data/app/models/formblocks/blocks/paragraph.rb +13 -0
- data/app/models/formblocks/blocks/phone.rb +13 -0
- data/app/models/formblocks/blocks/radio_group.rb +28 -0
- data/app/models/formblocks/blocks/text.rb +12 -0
- data/app/models/formblocks/blocks/textarea.rb +8 -0
- data/app/models/formblocks/blocks/url.rb +26 -0
- data/app/models/formblocks/csv_export.rb +47 -0
- data/app/models/formblocks/form.rb +197 -0
- data/app/models/formblocks/page.rb +79 -0
- data/app/models/formblocks/response.rb +48 -0
- data/app/models/formblocks/setting.rb +19 -0
- data/app/views/formblocks/blocks/_block.html.erb +102 -0
- data/app/views/formblocks/blocks/_palette.html.erb +16 -0
- data/app/views/formblocks/forms/_header.html.erb +19 -0
- data/app/views/formblocks/forms/edit.html.erb +38 -0
- data/app/views/formblocks/forms/index.html.erb +48 -0
- data/app/views/formblocks/forms/new.html.erb +26 -0
- data/app/views/formblocks/forms/published.html.erb +25 -0
- data/app/views/formblocks/forms/settings.html.erb +43 -0
- data/app/views/formblocks/pages/_page.html.erb +51 -0
- data/app/views/formblocks/public/blocks/_checkbox.html.erb +15 -0
- data/app/views/formblocks/public/blocks/_field.html.erb +13 -0
- data/app/views/formblocks/public/blocks/_heading.html.erb +1 -0
- data/app/views/formblocks/public/blocks/_hidden.html.erb +5 -0
- data/app/views/formblocks/public/blocks/_image.html.erb +3 -0
- data/app/views/formblocks/public/blocks/_paragraph.html.erb +1 -0
- data/app/views/formblocks/public/blocks/_radio_group.html.erb +17 -0
- data/app/views/formblocks/public/blocks/_text.html.erb +6 -0
- data/app/views/formblocks/public/blocks/_textarea.html.erb +5 -0
- data/app/views/formblocks/public/forms/not_found.html.erb +3 -0
- data/app/views/formblocks/public/forms/show.html.erb +37 -0
- data/app/views/formblocks/public/forms/thanks.html.erb +5 -0
- data/app/views/formblocks/responses/index.html.erb +45 -0
- data/app/views/formblocks/responses/show.html.erb +43 -0
- data/app/views/formblocks/settings/show.html.erb +19 -0
- data/app/views/formblocks/shared/_admin.html.erb +21 -0
- data/app/views/formblocks/shared/_brand_fields.html.erb +29 -0
- data/app/views/formblocks/shared/_errors.html.erb +7 -0
- data/app/views/layouts/formblocks/application.html.erb +18 -0
- data/app/views/layouts/formblocks/public.html.erb +20 -0
- data/config/locales/formblocks.en.yml +188 -0
- data/config/routes.rb +35 -0
- data/lib/formblocks/assets/admin.css +212 -0
- data/lib/formblocks/assets/admin.js +215 -0
- data/lib/formblocks/assets/public.css +66 -0
- data/lib/formblocks/assets/public.js +86 -0
- data/lib/formblocks/assets.rb +81 -0
- data/lib/formblocks/configuration.rb +93 -0
- data/lib/formblocks/engine.rb +33 -0
- data/lib/formblocks/seeds.rb +50 -0
- data/lib/formblocks/templates.rb +97 -0
- data/lib/formblocks/version.rb +5 -0
- data/lib/formblocks.rb +74 -0
- data/lib/generators/formblocks/install/install_generator.rb +43 -0
- data/lib/generators/formblocks/install/templates/create_formblocks_tables.rb.tt +65 -0
- data/lib/generators/formblocks/install/templates/initializer.rb.tt +69 -0
- data/lib/generators/formblocks/migration_helpers.rb +40 -0
- data/lib/tasks/formblocks_tasks.rake +10 -0
- metadata +199 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<section class="fb-page-card" id="<%= dom_id(page) %>">
|
|
2
|
+
<header class="fb-page-card__header">
|
|
3
|
+
<div>
|
|
4
|
+
<h2 class="fb-h2">
|
|
5
|
+
<%= page.thank_you? ? t('formblocks.forms.edit.thank_you') : t('formblocks.forms.edit.page', number: page.number) %>
|
|
6
|
+
</h2>
|
|
7
|
+
<% if page.thank_you? %><p class="fb-muted fb-small"><%= t('formblocks.forms.edit.thank_you_hint') %></p><% end %>
|
|
8
|
+
</div>
|
|
9
|
+
<% if page.step? %>
|
|
10
|
+
<div class="fb-actions">
|
|
11
|
+
<%= button_to move_form_page_path(form, page, direction: 'up'), method: :patch, class: 'fb-iconbtn',
|
|
12
|
+
title: t('formblocks.actions.move_up'), 'aria-label': t('formblocks.actions.move_up'),
|
|
13
|
+
disabled: page.number == 1, form: { data: { turbo_action: 'replace' } } do %><%= fb_icon(:up) %><% end %>
|
|
14
|
+
<%= button_to move_form_page_path(form, page, direction: 'down'), method: :patch, class: 'fb-iconbtn',
|
|
15
|
+
title: t('formblocks.actions.move_down'), 'aria-label': t('formblocks.actions.move_down'),
|
|
16
|
+
disabled: page.last_step?, form: { data: { turbo_action: 'replace' } } do %><%= fb_icon(:down) %><% end %>
|
|
17
|
+
<% if form.steps.size > 1 %>
|
|
18
|
+
<%= button_to form_page_path(form, page), method: :delete, class: 'fb-iconbtn fb-iconbtn--danger',
|
|
19
|
+
title: t('formblocks.actions.delete'), 'aria-label': t('formblocks.actions.delete'),
|
|
20
|
+
form: { data: { turbo_confirm: t('formblocks.actions.confirm_delete_page'), turbo_action: 'replace' } } do %><%= fb_icon(:trash) %><% end %>
|
|
21
|
+
<% end %>
|
|
22
|
+
</div>
|
|
23
|
+
<% end %>
|
|
24
|
+
</header>
|
|
25
|
+
|
|
26
|
+
<div class="fb-blocks" data-controller="fb-sortable" data-fb-sortable-url-value="<%= reorder_form_page_blocks_path(form, page) %>">
|
|
27
|
+
<% page.blocks.each do |block| %>
|
|
28
|
+
<%= render 'formblocks/blocks/block', block: block %>
|
|
29
|
+
<% end %>
|
|
30
|
+
</div>
|
|
31
|
+
<% if page.blocks.empty? %>
|
|
32
|
+
<p class="fb-empty-page fb-muted"><%= t('formblocks.forms.edit.empty_page') %></p>
|
|
33
|
+
<% end %>
|
|
34
|
+
|
|
35
|
+
<%= render 'formblocks/blocks/palette', page: page, form: form %>
|
|
36
|
+
|
|
37
|
+
<% if page.step? %>
|
|
38
|
+
<footer class="fb-page-card__footer">
|
|
39
|
+
<%= form_with model: page, url: form_page_path(form, page), method: :patch, class: 'fb-button-editor',
|
|
40
|
+
data: { controller: 'fb-autosave',
|
|
41
|
+
action: 'input->fb-autosave#queue change->fb-autosave#save turbo:submit-end->fb-autosave#finished' } do |f| %>
|
|
42
|
+
<%= f.text_field :button_text, value: page.button_label, class: 'fb-button-preview', maxlength: 60, autocomplete: 'off',
|
|
43
|
+
'aria-label': t('formblocks.forms.edit.button_text_label'),
|
|
44
|
+
data: { controller: 'fb-autosize', action: 'input->fb-autosize#resize' } %>
|
|
45
|
+
<span class="fb-autosave-status" data-fb-autosave-target="status" aria-live="polite"></span>
|
|
46
|
+
<button class="fb-btn fb-btn--sm fb-nojs" type="submit"><%= t('formblocks.actions.save') %></button>
|
|
47
|
+
<% end %>
|
|
48
|
+
<p class="fb-muted fb-small"><%= t('formblocks.forms.edit.button_hint') %></p>
|
|
49
|
+
</footer>
|
|
50
|
+
<% end %>
|
|
51
|
+
</section>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<% id = "fb_#{block.key}" %>
|
|
2
|
+
<% error = response&.error_for(block) %>
|
|
3
|
+
<div class="fb-field fb-field--check <%= 'fb-field--error' if error %>">
|
|
4
|
+
<label class="fb-checkbox" for="<%= id %>">
|
|
5
|
+
<%= hidden_field_tag "answers[#{block.key}]", '0', id: nil %>
|
|
6
|
+
<%= check_box_tag "answers[#{block.key}]", '1', response&.answer(block) == true, id: id, required: block.required?,
|
|
7
|
+
'aria-describedby': (error ? "#{id}_error" : nil) %>
|
|
8
|
+
<span>
|
|
9
|
+
<%= block.label %>
|
|
10
|
+
<% if block.required? %><span class="fb-required" aria-hidden="true">*</span><% end %>
|
|
11
|
+
</span>
|
|
12
|
+
</label>
|
|
13
|
+
<% if block.help_text.present? %><p class="fb-help"><%= block.help_text %></p><% end %>
|
|
14
|
+
<% if error %><p class="fb-error" id="<%= id %>_error" role="alert"><%= block.label %> <%= error %></p><% end %>
|
|
15
|
+
</div>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<%# Shared frame for an input: label, the control (yielded), help text and error. %>
|
|
2
|
+
<% error = response&.error_for(block) %>
|
|
3
|
+
<% describe = [(block.help_text.present? ? "#{id}_help" : nil), (error ? "#{id}_error" : nil)].compact %>
|
|
4
|
+
<div class="fb-field <%= 'fb-field--error' if error %>">
|
|
5
|
+
<label class="fb-label" for="<%= id %>">
|
|
6
|
+
<%= block.label %>
|
|
7
|
+
<% if block.required? %><span class="fb-required" aria-hidden="true">*</span>
|
|
8
|
+
<% else %><span class="fb-optional"><%= t('formblocks.public.optional') %></span><% end %>
|
|
9
|
+
</label>
|
|
10
|
+
<%= yield(describe.join(' ').presence) %>
|
|
11
|
+
<% if block.help_text.present? %><p class="fb-help" id="<%= id %>_help"><%= block.help_text %></p><% end %>
|
|
12
|
+
<% if error %><p class="fb-error" id="<%= id %>_error" role="alert"><%= block.label %> <%= error %></p><% end %>
|
|
13
|
+
</div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<h2 class="fb-heading"><%= block.content %></h2>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<%# The stored answer on a re-render, else the URL's query parameter, else the block's default. %>
|
|
2
|
+
<% value = if response&.answers&.key?(block.key) then response.answer(block)
|
|
3
|
+
elsif params[block.key].is_a?(String) then params[block.key]
|
|
4
|
+
else block.default_value end %>
|
|
5
|
+
<%= hidden_field_tag "answers[#{block.key}]", value, id: nil %>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<div class="fb-paragraph"><%= simple_format(block.content) %></div>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<% id = "fb_#{block.key}" %>
|
|
2
|
+
<% error = response&.error_for(block) %>
|
|
3
|
+
<fieldset class="fb-field fb-field--group <%= 'fb-field--error' if error %>" <%= "aria-describedby=#{id}_error" if error %>>
|
|
4
|
+
<legend class="fb-label">
|
|
5
|
+
<%= block.label %>
|
|
6
|
+
<% if block.required? %><span class="fb-required" aria-hidden="true">*</span>
|
|
7
|
+
<% else %><span class="fb-optional"><%= t('formblocks.public.optional') %></span><% end %>
|
|
8
|
+
</legend>
|
|
9
|
+
<% Array(block.options).each_with_index do |option, index| %>
|
|
10
|
+
<label class="fb-radio" for="<%= id %>_<%= index %>">
|
|
11
|
+
<%= radio_button_tag "answers[#{block.key}]", option, response&.answer(block) == option, id: "#{id}_#{index}", required: block.required? %>
|
|
12
|
+
<span><%= option %></span>
|
|
13
|
+
</label>
|
|
14
|
+
<% end %>
|
|
15
|
+
<% if block.help_text.present? %><p class="fb-help"><%= block.help_text %></p><% end %>
|
|
16
|
+
<% if error %><p class="fb-error" id="<%= id %>_error" role="alert"><%= block.label %> <%= error %></p><% end %>
|
|
17
|
+
</fieldset>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<% id = "fb_#{block.key}" %>
|
|
2
|
+
<%= render layout: 'formblocks/public/blocks/field', locals: { block: block, response: response, id: id } do |describedby| %>
|
|
3
|
+
<%= text_field_tag "answers[#{block.key}]", response&.answer(block), id: id, type: block.html_input_type, class: 'fb-input',
|
|
4
|
+
required: block.required?, placeholder: block.placeholder.presence, autocomplete: block.autocomplete,
|
|
5
|
+
'aria-describedby': describedby %>
|
|
6
|
+
<% end %>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<% id = "fb_#{block.key}" %>
|
|
2
|
+
<%= render layout: 'formblocks/public/blocks/field', locals: { block: block, response: response, id: id } do |describedby| %>
|
|
3
|
+
<%= text_area_tag "answers[#{block.key}]", response&.answer(block), id: id, class: 'fb-input', rows: 4,
|
|
4
|
+
required: block.required?, placeholder: block.placeholder.presence, 'aria-describedby': describedby %>
|
|
5
|
+
<% end %>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<% steps = @form.steps %>
|
|
2
|
+
<%= form_with url: main_app.formblocks_form_path(@form.slug, preview: params[:preview].presence), method: :post,
|
|
3
|
+
class: 'fb-form', data: { controller: 'fb-pager', fb_pager_step_label_value: t('formblocks.public.step', current: '%{current}', total: '%{total}') } do %>
|
|
4
|
+
<% if @rate_limited %>
|
|
5
|
+
<p class="fb-alert" role="alert"><%= t('formblocks.errors.rate_limited') %></p>
|
|
6
|
+
<% elsif @response.errors.any? %>
|
|
7
|
+
<p class="fb-alert" role="alert"><%= t('formblocks.public.fix_errors') %></p>
|
|
8
|
+
<% end %>
|
|
9
|
+
|
|
10
|
+
<%# The honeypot: off-screen, unlabelled for assistive tech, and never filled by a person. %>
|
|
11
|
+
<div class="fb-hp" aria-hidden="true">
|
|
12
|
+
<label for="fb_website">Website</label>
|
|
13
|
+
<input type="text" id="fb_website" name="fb_website" tabindex="-1" autocomplete="off">
|
|
14
|
+
</div>
|
|
15
|
+
<%= hidden_field_tag 'fb_referrer', @referrer, id: nil %>
|
|
16
|
+
|
|
17
|
+
<% steps.each_with_index do |page, index| %>
|
|
18
|
+
<% has_errors = page.blocks.any? { |block| block.input? && @response.error_for(block) } %>
|
|
19
|
+
<fieldset class="fb-step" data-fb-pager-target="step" <%= 'data-fb-pager-errors' if has_errors %>>
|
|
20
|
+
<legend class="fb-sr-only"><%= t('formblocks.public.step', current: index + 1, total: steps.size) %></legend>
|
|
21
|
+
<% page.blocks.each do |block| %>
|
|
22
|
+
<%= render partial: block.public_partial, locals: { block: block, response: @response } %>
|
|
23
|
+
<% end %>
|
|
24
|
+
<div class="fb-step__nav">
|
|
25
|
+
<% if index.positive? %>
|
|
26
|
+
<button type="button" class="fb-btn fb-btn--secondary" data-action="fb-pager#back" data-fb-pager-target="nav" hidden><%= t('formblocks.public.back') %></button>
|
|
27
|
+
<% end %>
|
|
28
|
+
<% if page.last_step? %>
|
|
29
|
+
<button type="submit" class="fb-btn fb-btn--primary"><%= page.button_label %></button>
|
|
30
|
+
<% else %>
|
|
31
|
+
<button type="button" class="fb-btn fb-btn--primary" data-action="fb-pager#next" data-fb-pager-target="nav" hidden><%= page.button_label %></button>
|
|
32
|
+
<% end %>
|
|
33
|
+
</div>
|
|
34
|
+
</fieldset>
|
|
35
|
+
<% end %>
|
|
36
|
+
<p class="fb-progress" data-fb-pager-target="progress" hidden></p>
|
|
37
|
+
<% end %>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<% content_for :title, "#{@form.title} · #{t('.title')}" %>
|
|
2
|
+
<%= fb_admin_shell do %>
|
|
3
|
+
<%= render 'formblocks/forms/header', form: @form, tab: :responses %>
|
|
4
|
+
|
|
5
|
+
<header class="fb-header">
|
|
6
|
+
<h2 class="fb-h2"><%= t('.count', count: @form.responses_count) %></h2>
|
|
7
|
+
<% if @form.responses_count.positive? %>
|
|
8
|
+
<%= link_to form_responses_path(@form, format: :csv), class: 'fb-btn', data: { turbo: false } do %>
|
|
9
|
+
<%= fb_icon(:download) %> <%= t('.download_csv') %>
|
|
10
|
+
<% end %>
|
|
11
|
+
<% end %>
|
|
12
|
+
</header>
|
|
13
|
+
|
|
14
|
+
<% if @responses.empty? %>
|
|
15
|
+
<div class="fb-card fb-empty"><p><%= t('.empty') %></p></div>
|
|
16
|
+
<% else %>
|
|
17
|
+
<div class="fb-card fb-card--flush">
|
|
18
|
+
<table class="fb-table">
|
|
19
|
+
<thead>
|
|
20
|
+
<tr>
|
|
21
|
+
<th><%= t('.submitted') %></th>
|
|
22
|
+
<% @columns.each do |block| %><th><%= block.label %></th><% end %>
|
|
23
|
+
<th><span class="fb-sr-only"><%= t('formblocks.actions.actions') %></span></th>
|
|
24
|
+
</tr>
|
|
25
|
+
</thead>
|
|
26
|
+
<tbody>
|
|
27
|
+
<% @responses.each do |response| %>
|
|
28
|
+
<tr id="<%= dom_id(response) %>">
|
|
29
|
+
<td><%= link_to fb_time(response.created_at), form_response_path(@form, response), class: 'fb-link fb-link--strong' %></td>
|
|
30
|
+
<% @columns.each do |block| %>
|
|
31
|
+
<td class="fb-cell-clip">
|
|
32
|
+
<%= block.display_answer(response.answer(block)).presence || tag.span(t('formblocks.responses.show.empty'), class: 'fb-muted') %>
|
|
33
|
+
</td>
|
|
34
|
+
<% end %>
|
|
35
|
+
<td class="fb-table__actions"><%= link_to t('.view'), form_response_path(@form, response), class: 'fb-btn fb-btn--sm' %></td>
|
|
36
|
+
</tr>
|
|
37
|
+
<% end %>
|
|
38
|
+
</tbody>
|
|
39
|
+
</table>
|
|
40
|
+
</div>
|
|
41
|
+
<% if @more %>
|
|
42
|
+
<p class="fb-more"><%= link_to t('.more'), form_responses_path(@form, page: @page + 1), class: 'fb-btn' %></p>
|
|
43
|
+
<% end %>
|
|
44
|
+
<% end %>
|
|
45
|
+
<% end %>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<% content_for :title, "#{@form.title} · #{t('.title')}" %>
|
|
2
|
+
<%= fb_admin_shell do %>
|
|
3
|
+
<%= render 'formblocks/forms/header', form: @form, tab: :responses %>
|
|
4
|
+
|
|
5
|
+
<p><%= link_to form_responses_path(@form), class: 'fb-backlink' do %><%= fb_icon(:back) %> <%= t('.all') %><% end %></p>
|
|
6
|
+
|
|
7
|
+
<% blocks = @form.input_blocks %>
|
|
8
|
+
<% orphans = @response.answers.to_h.keys - blocks.map(&:key) %>
|
|
9
|
+
<div class="fb-card">
|
|
10
|
+
<dl class="fb-dl">
|
|
11
|
+
<% blocks.each do |block| %>
|
|
12
|
+
<div class="fb-dl__row">
|
|
13
|
+
<dt><%= block.label.presence || block.key %></dt>
|
|
14
|
+
<dd class="fb-pre"><%= block.display_answer(@response.answer(block)).presence || tag.span(t('.empty'), class: 'fb-muted') %></dd>
|
|
15
|
+
</div>
|
|
16
|
+
<% end %>
|
|
17
|
+
<% orphans.each do |key| %>
|
|
18
|
+
<div class="fb-dl__row">
|
|
19
|
+
<dt class="fb-mono"><%= key %></dt>
|
|
20
|
+
<dd class="fb-pre"><%= @response.answers[key].to_s.presence || tag.span(t('.empty'), class: 'fb-muted') %></dd>
|
|
21
|
+
</div>
|
|
22
|
+
<% end %>
|
|
23
|
+
</dl>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div class="fb-card">
|
|
27
|
+
<dl class="fb-dl fb-dl--meta">
|
|
28
|
+
<div class="fb-dl__row"><dt><%= t('.submitted_at') %></dt><dd><%= fb_time(@response.created_at) %></dd></div>
|
|
29
|
+
<% if @response.page_url.present? %>
|
|
30
|
+
<div class="fb-dl__row"><dt><%= t('.page_url') %></dt><dd class="fb-break"><%= @response.page_url %></dd></div>
|
|
31
|
+
<% end %>
|
|
32
|
+
<% if @response.user_agent.present? %>
|
|
33
|
+
<div class="fb-dl__row"><dt><%= t('.user_agent') %></dt><dd class="fb-break fb-small"><%= @response.user_agent %></dd></div>
|
|
34
|
+
<% end %>
|
|
35
|
+
<% if @response.locale.present? %>
|
|
36
|
+
<div class="fb-dl__row"><dt><%= t('.locale') %></dt><dd><%= @response.locale %></dd></div>
|
|
37
|
+
<% end %>
|
|
38
|
+
</dl>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<%= button_to t('formblocks.actions.delete'), form_response_path(@form, @response), method: :delete, class: 'fb-btn fb-btn--danger',
|
|
42
|
+
form: { data: { turbo_confirm: t('formblocks.actions.confirm_delete') } } %>
|
|
43
|
+
<% end %>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<% content_for :title, t('.title') %>
|
|
2
|
+
<%= fb_admin_shell do %>
|
|
3
|
+
<header class="fb-header">
|
|
4
|
+
<h1 class="fb-h1"><%= t('.title') %></h1>
|
|
5
|
+
</header>
|
|
6
|
+
<p class="fb-muted"><%= t('.hint') %></p>
|
|
7
|
+
|
|
8
|
+
<%= form_with model: @setting, url: settings_path, method: :patch, class: 'fb-stack' do |f| %>
|
|
9
|
+
<%= render 'formblocks/shared/errors', record: @setting %>
|
|
10
|
+
<section class="fb-card">
|
|
11
|
+
<%= render 'formblocks/shared/brand_fields', f: f, record: @setting, logo_hint: t('.logo_hint'),
|
|
12
|
+
fallbacks: { primary_color: Formblocks.config.default_primary_color,
|
|
13
|
+
button_text_color: Formblocks.config.default_button_text_color } %>
|
|
14
|
+
</section>
|
|
15
|
+
<div class="fb-form-actions">
|
|
16
|
+
<%= f.submit t('formblocks.actions.save'), class: 'fb-btn fb-btn--primary' %>
|
|
17
|
+
</div>
|
|
18
|
+
<% end %>
|
|
19
|
+
<% end %>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<%# The admin's own shell, rendered by the views (through fb_admin_shell) rather
|
|
2
|
+
than the layout. That is deliberate: `config.admin_layout` and
|
|
3
|
+
`config.base_controller_class` let a host replace the layout, and assets
|
|
4
|
+
declared in a layout the host replaces vanish. Declaring them here means
|
|
5
|
+
every layout works, host or gem.
|
|
6
|
+
|
|
7
|
+
The `fb-admin` wrapper is what scopes admin.css, so it encloses the content. %>
|
|
8
|
+
<%= formblocks_assets(:admin) %>
|
|
9
|
+
<div class="fb-admin">
|
|
10
|
+
<nav class="fb-nav" aria-label="<%= t('formblocks.title') %>">
|
|
11
|
+
<%= link_to t('formblocks.nav.forms'), forms_path,
|
|
12
|
+
class: class_names('fb-nav__link', 'is-active': controller_name != 'settings') %>
|
|
13
|
+
<%= link_to t('formblocks.nav.settings'), settings_path,
|
|
14
|
+
class: class_names('fb-nav__link', 'is-active': controller_name == 'settings') %>
|
|
15
|
+
</nav>
|
|
16
|
+
<div class="fb-container">
|
|
17
|
+
<% if notice.present? %><p class="fb-flash fb-flash--notice" role="status"><%= notice %></p><% end %>
|
|
18
|
+
<% if alert.present? %><p class="fb-flash fb-flash--alert" role="alert"><%= alert %></p><% end %>
|
|
19
|
+
<%= content %>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<%# Colors and logo, shared by a form's settings and the global settings.
|
|
2
|
+
`fallbacks` maps each color attribute to the value it inherits when blank. %>
|
|
3
|
+
<div class="fb-grid">
|
|
4
|
+
<% fallbacks.each do |attribute, fallback| %>
|
|
5
|
+
<div class="fb-field" data-controller="fb-color">
|
|
6
|
+
<%= f.label attribute, t("formblocks.fields.#{attribute}"), class: 'fb-label' %>
|
|
7
|
+
<div class="fb-color">
|
|
8
|
+
<input type="color" class="fb-color__picker" value="<%= record.public_send(attribute).presence || fallback %>"
|
|
9
|
+
data-fb-color-target="picker" data-action="input->fb-color#pick"
|
|
10
|
+
aria-label="<%= t("formblocks.fields.#{attribute}") %>">
|
|
11
|
+
<%= f.text_field attribute, class: 'fb-input fb-input--mono', placeholder: fallback, maxlength: 7,
|
|
12
|
+
autocomplete: 'off', spellcheck: false,
|
|
13
|
+
data: { fb_color_target: 'text', action: 'input->fb-color#type' } %>
|
|
14
|
+
</div>
|
|
15
|
+
<p class="fb-hint"><%= t('formblocks.fields.color_inherit', color: fallback) %></p>
|
|
16
|
+
</div>
|
|
17
|
+
<% end %>
|
|
18
|
+
</div>
|
|
19
|
+
<% if Formblocks.attachments? %>
|
|
20
|
+
<div class="fb-field">
|
|
21
|
+
<%= f.label :logo, t('formblocks.fields.logo'), class: 'fb-label' %>
|
|
22
|
+
<% if fb_stored?(record.logo) %>
|
|
23
|
+
<div class="fb-logo-preview"><%= image_tag fb_attachment_url(record.logo), alt: '' %></div>
|
|
24
|
+
<label class="fb-check"><%= f.check_box :remove_logo %> <%= t('formblocks.forms.settings.logo_remove') %></label>
|
|
25
|
+
<% end %>
|
|
26
|
+
<%= f.file_field :logo, accept: 'image/*', class: 'fb-input fb-input--file' %>
|
|
27
|
+
<% if logo_hint.present? %><p class="fb-hint"><%= logo_hint %></p><% end %>
|
|
28
|
+
</div>
|
|
29
|
+
<% end %>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="<%= I18n.locale %>">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title><%= [content_for(:title), t('formblocks.title'), Formblocks.app_name].compact_blank.join(' · ') %></title>
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
7
|
+
<%# Turbo 8 morphs page refreshes, so a builder change (add a block, move a page) re-renders in place without losing scroll position. %>
|
|
8
|
+
<meta name="turbo-refresh-method" content="morph">
|
|
9
|
+
<meta name="turbo-refresh-scroll" content="preserve">
|
|
10
|
+
<%= csrf_meta_tags %>
|
|
11
|
+
<%= csp_meta_tag %>
|
|
12
|
+
<%# The stylesheet and script are declared by the views (formblocks/shared/_admin),
|
|
13
|
+
not here, so they survive a host replacing this layout via config.admin_layout. %>
|
|
14
|
+
</head>
|
|
15
|
+
<body class="fb-page">
|
|
16
|
+
<%= yield %>
|
|
17
|
+
</body>
|
|
18
|
+
</html>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="<%= I18n.locale %>">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title><%= [@form&.title, Formblocks.app_name].compact_blank.join(' · ') %></title>
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
7
|
+
<%= csrf_meta_tags %>
|
|
8
|
+
<%= csp_meta_tag %>
|
|
9
|
+
<%= formblocks_assets(:public) %>
|
|
10
|
+
<%= formblocks_brand_style(@form) if @form %>
|
|
11
|
+
</head>
|
|
12
|
+
<body class="fb-public fb-brand">
|
|
13
|
+
<main class="fb-public__main">
|
|
14
|
+
<% if @form && (logo = @form.effective_logo) %>
|
|
15
|
+
<div class="fb-public__logo"><%= image_tag fb_attachment_url(logo), alt: Formblocks.app_name %></div>
|
|
16
|
+
<% end %>
|
|
17
|
+
<%= yield %>
|
|
18
|
+
</main>
|
|
19
|
+
</body>
|
|
20
|
+
</html>
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
en:
|
|
2
|
+
formblocks:
|
|
3
|
+
title: Forms
|
|
4
|
+
forbidden: Forbidden. Set Formblocks.config.authorize_admin to grant access.
|
|
5
|
+
nav:
|
|
6
|
+
forms: Forms
|
|
7
|
+
settings: Settings
|
|
8
|
+
status:
|
|
9
|
+
draft: Draft
|
|
10
|
+
published: Published
|
|
11
|
+
actions:
|
|
12
|
+
actions: Actions
|
|
13
|
+
save: Save
|
|
14
|
+
cancel: Cancel
|
|
15
|
+
delete: Delete
|
|
16
|
+
delete_form: Delete this form
|
|
17
|
+
duplicate: Duplicate
|
|
18
|
+
back: Back
|
|
19
|
+
move_up: Move up
|
|
20
|
+
move_down: Move down
|
|
21
|
+
confirm_delete: Delete this? This cannot be undone.
|
|
22
|
+
confirm_delete_form: Delete this form and all of its responses? This cannot be undone.
|
|
23
|
+
confirm_delete_page: Delete this page and its blocks?
|
|
24
|
+
confirm_delete_block: Delete this block?
|
|
25
|
+
flash:
|
|
26
|
+
form_created: Form created.
|
|
27
|
+
form_updated: Settings saved.
|
|
28
|
+
form_deleted: Form deleted.
|
|
29
|
+
form_duplicated: Form duplicated.
|
|
30
|
+
form_unpublished: Form unpublished. It is no longer reachable at its public URL.
|
|
31
|
+
settings_updated: Settings saved.
|
|
32
|
+
response_deleted: Response deleted.
|
|
33
|
+
forms:
|
|
34
|
+
index:
|
|
35
|
+
title: Forms
|
|
36
|
+
new: New form
|
|
37
|
+
empty: No forms yet. Create your first one.
|
|
38
|
+
columns:
|
|
39
|
+
title: Title
|
|
40
|
+
status: Status
|
|
41
|
+
responses: Responses
|
|
42
|
+
updated: Updated
|
|
43
|
+
new:
|
|
44
|
+
title: New form
|
|
45
|
+
blank: Blank form
|
|
46
|
+
blank_description: Start from scratch with one empty page.
|
|
47
|
+
start_blank: Start blank
|
|
48
|
+
use_template: Use template
|
|
49
|
+
untitled: Untitled form
|
|
50
|
+
summary:
|
|
51
|
+
one: "%{pages} page · %{fields} fields"
|
|
52
|
+
other: "%{pages} pages · %{fields} fields"
|
|
53
|
+
edit:
|
|
54
|
+
page: "Page %{number}"
|
|
55
|
+
thank_you: Thank you page
|
|
56
|
+
thank_you_hint: Shown after a successful submission. Content blocks only.
|
|
57
|
+
add_page: Add page
|
|
58
|
+
add_block: Add block
|
|
59
|
+
content_blocks: Content
|
|
60
|
+
input_blocks: Inputs
|
|
61
|
+
empty_page: This page has no blocks yet. Add one below.
|
|
62
|
+
button_text_label: Button text
|
|
63
|
+
button_hint: Click the button to change its text.
|
|
64
|
+
drag: Drag to reorder
|
|
65
|
+
preview: Preview
|
|
66
|
+
publish: Publish
|
|
67
|
+
unpublish: Unpublish
|
|
68
|
+
published_hint: "Live at %{url}"
|
|
69
|
+
draft_hint: Draft — visitors cannot see this form until you publish it.
|
|
70
|
+
tabs:
|
|
71
|
+
builder: Builder
|
|
72
|
+
settings: Settings
|
|
73
|
+
responses: Responses
|
|
74
|
+
settings:
|
|
75
|
+
general: General
|
|
76
|
+
branding: Branding
|
|
77
|
+
branding_hint: Leave a color blank to inherit the global setting.
|
|
78
|
+
slug_hint: Lowercase letters, numbers and dashes. Leave blank to generate one from the title.
|
|
79
|
+
logo_remove: Remove logo
|
|
80
|
+
danger: Danger zone
|
|
81
|
+
delete_hint: Deleting a form also deletes all of its responses.
|
|
82
|
+
published:
|
|
83
|
+
title: Your form is live
|
|
84
|
+
message: Anyone with the link can now fill it in.
|
|
85
|
+
url_label: Public URL
|
|
86
|
+
copy: Copy
|
|
87
|
+
copied: Copied!
|
|
88
|
+
open: Open form
|
|
89
|
+
back_to_builder: Back to builder
|
|
90
|
+
view_responses: View responses
|
|
91
|
+
fields:
|
|
92
|
+
title: Title
|
|
93
|
+
slug: Slug
|
|
94
|
+
primary_color: Primary color
|
|
95
|
+
button_text_color: Button text color
|
|
96
|
+
logo: Logo
|
|
97
|
+
label: Label
|
|
98
|
+
placeholder: Placeholder
|
|
99
|
+
help_text: Help text
|
|
100
|
+
required: Required
|
|
101
|
+
content: Text
|
|
102
|
+
options: Options
|
|
103
|
+
options_hint: One option per line.
|
|
104
|
+
key: Field name
|
|
105
|
+
key_hint: The name the value is stored under. A matching query parameter on the public URL fills it (for example ?utm_source=…).
|
|
106
|
+
value: Default value
|
|
107
|
+
image: Image
|
|
108
|
+
alt_text: Alt text
|
|
109
|
+
remove_image: Remove image
|
|
110
|
+
attachments_off: Image uploads need Active Storage in your app.
|
|
111
|
+
color_inherit: "Blank inherits %{color}"
|
|
112
|
+
blocks:
|
|
113
|
+
heading:
|
|
114
|
+
name: Heading
|
|
115
|
+
paragraph:
|
|
116
|
+
name: Paragraph
|
|
117
|
+
image:
|
|
118
|
+
name: Image
|
|
119
|
+
name:
|
|
120
|
+
name: Name
|
|
121
|
+
email:
|
|
122
|
+
name: Email
|
|
123
|
+
phone:
|
|
124
|
+
name: Phone
|
|
125
|
+
url:
|
|
126
|
+
name: URL
|
|
127
|
+
text:
|
|
128
|
+
name: Short text
|
|
129
|
+
textarea:
|
|
130
|
+
name: Long text
|
|
131
|
+
hidden:
|
|
132
|
+
name: Hidden field
|
|
133
|
+
checkbox:
|
|
134
|
+
name: Checkbox
|
|
135
|
+
radio_group:
|
|
136
|
+
name: Radio group
|
|
137
|
+
defaults:
|
|
138
|
+
heading: Heading
|
|
139
|
+
paragraph: Write something here.
|
|
140
|
+
option: "Option %{n}"
|
|
141
|
+
"yes": "Yes"
|
|
142
|
+
"no": "No"
|
|
143
|
+
responses:
|
|
144
|
+
index:
|
|
145
|
+
title: Responses
|
|
146
|
+
empty: No responses yet.
|
|
147
|
+
download_csv: Download CSV
|
|
148
|
+
submitted: Submitted
|
|
149
|
+
view: View
|
|
150
|
+
more: Load more
|
|
151
|
+
count:
|
|
152
|
+
one: 1 response
|
|
153
|
+
other: "%{count} responses"
|
|
154
|
+
show:
|
|
155
|
+
title: Response
|
|
156
|
+
all: All responses
|
|
157
|
+
submitted_at: Submitted
|
|
158
|
+
page_url: Came from
|
|
159
|
+
user_agent: Browser
|
|
160
|
+
locale: Locale
|
|
161
|
+
empty: (empty)
|
|
162
|
+
csv:
|
|
163
|
+
submitted_at: Submitted at
|
|
164
|
+
settings:
|
|
165
|
+
show:
|
|
166
|
+
title: Settings
|
|
167
|
+
hint: Defaults for every form. A form can override each of these in its own settings.
|
|
168
|
+
logo_hint: Shown at the top of every form that has no logo of its own.
|
|
169
|
+
public:
|
|
170
|
+
next: Next
|
|
171
|
+
back: Back
|
|
172
|
+
submit: Submit
|
|
173
|
+
step: "Step %{current} of %{total}"
|
|
174
|
+
optional: optional
|
|
175
|
+
fix_errors: Please check the highlighted fields.
|
|
176
|
+
thanks_title: Thank you!
|
|
177
|
+
thanks_message: Your response has been recorded.
|
|
178
|
+
not_found: This form is not available.
|
|
179
|
+
errors:
|
|
180
|
+
required: is required
|
|
181
|
+
invalid_email: is not a valid email address
|
|
182
|
+
invalid_url: is not a valid URL (include http:// or https://)
|
|
183
|
+
invalid_option: is not one of the options
|
|
184
|
+
must_be_checked: must be checked
|
|
185
|
+
too_long: is too long
|
|
186
|
+
rate_limited: Too many submissions. Please wait a moment and try again.
|
|
187
|
+
not_an_image: must be an image
|
|
188
|
+
file_too_large: "is too large (max %{size} MB)"
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Formblocks::Engine.routes.draw do
|
|
4
|
+
# The engine's own CSS and JavaScript, served same-origin (see Formblocks::Assets).
|
|
5
|
+
get 'assets/:name', to: 'assets#show', as: :engine_asset, format: false, constraints: { name: /[\w.-]+/ }
|
|
6
|
+
|
|
7
|
+
# Global brand settings — declared before the forms so `/settings` is never
|
|
8
|
+
# read as a form id.
|
|
9
|
+
resource :settings, only: %i[show update]
|
|
10
|
+
|
|
11
|
+
# Flat, human URLs: the mount path IS the forms index. /forms is the list,
|
|
12
|
+
# /forms/new the template picker, /forms/12/edit the builder.
|
|
13
|
+
resources :forms, path: '', except: :show do
|
|
14
|
+
member do
|
|
15
|
+
post :duplicate
|
|
16
|
+
post :publish
|
|
17
|
+
post :unpublish
|
|
18
|
+
get :published
|
|
19
|
+
get :settings
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
resources :pages, only: %i[create update destroy] do
|
|
23
|
+
member { patch :move }
|
|
24
|
+
|
|
25
|
+
resources :blocks, only: %i[create update destroy] do
|
|
26
|
+
member { patch :move }
|
|
27
|
+
collection { patch :reorder }
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
resources :responses, only: %i[index show destroy]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
root to: 'forms#index'
|
|
35
|
+
end
|