Commit 0fc5d8f9 by jasl

Add nested form to dummy

parent 1404df14
...@@ -5,8 +5,13 @@ inherit_gem: ...@@ -5,8 +5,13 @@ inherit_gem:
AllCops: AllCops:
TargetRubyVersion: 2.4 TargetRubyVersion: 2.4
Exclude: Exclude:
- test/dummy/db/schema.rb - bin/**/*
- test/dummy/bin/**/*
- test/dummy/mruby/**/* - test/dummy/mruby/**/*
- test/dummy/db/schema.rb
Metrics/LineLength:
Max: 150
# frozen_string_literal: true # frozen_string_literal: true
Style/FrozenStringLiteralComment: Style/FrozenStringLiteralComment:
...@@ -127,3 +132,36 @@ Layout/EndAlignment: ...@@ -127,3 +132,36 @@ Layout/EndAlignment:
# Use my_method(my_arg) not my_method( my_arg ) or my_method my_arg. # Use my_method(my_arg) not my_method( my_arg ) or my_method my_arg.
Lint/RequireParentheses: Lint/RequireParentheses:
Enabled: true Enabled: true
Style/Documentation:
Enabled: false
Metrics/MethodLength:
Enabled: false
Metrics/AbcSize:
Enabled: false
Metrics/ParameterLists:
Enabled: false
Metrics/BlockLength:
Enabled: false
Lint/HandleExceptions:
Enabled: false
Metrics/ClassLength:
Enabled: false
Layout/EmptyLinesAroundArguments:
Enabled: false
Style/ClassAndModuleChildren:
Enabled: false
Naming/UncommunicativeMethodParamName:
Enabled: false
Style/IfUnlessModifier:
Enabled: false
...@@ -2,18 +2,13 @@ ...@@ -2,18 +2,13 @@
module Workflows module Workflows
class Fields::ApplicationController < ApplicationController class Fields::ApplicationController < ApplicationController
before_action :set_form
before_action :set_field before_action :set_field
protected protected
# Use callbacks to share common setup or constraints between actions. # Use callbacks to share common setup or constraints between actions.
def set_form
@form = @workflow.form
end
def set_field def set_field
@field = @form.fields.find(params[:field_id]) @field = @workflow.fields.find(params[:field_id])
end end
end end
end end
...@@ -22,7 +22,7 @@ class Workflows::FieldsController < Workflows::ApplicationController ...@@ -22,7 +22,7 @@ class Workflows::FieldsController < Workflows::ApplicationController
def create def create
@field = @form.fields.build(field_params) @field = @form.fields.build(field_params)
if @field.save if @field.save!
redirect_to workflow_fields_url(@workflow), notice: "Field was successfully created." redirect_to workflow_fields_url(@workflow), notice: "Field was successfully created."
else else
render :new render :new
......
...@@ -20,11 +20,9 @@ module Workflows ...@@ -20,11 +20,9 @@ module Workflows
def fire def fire
@form_record = @virtual_model.load(@instance.payload) @form_record = @virtual_model.load(@instance.payload)
@form_record.assign_attributes(form_record_params) @form_record.assign_attributes(form_record_params)
@transition_valid = @token.place.output_transition.options.valid? @transition_valid = @token.place.output_transition.options.valid?
if @form_record.valid? && @transition_valid if @form_record.valid? && @transition_valid
@instance.update! payload: (@instance.payload || {}).merge(@form_record.serializable_hash) @instance.update! payload: (@instance.payload || {}).merge(@form_record.serializable_hash)
@token.place.output_transition.fire(@token) @token.place.output_transition.fire(@token)
......
# frozen_string_literal: true
module Workflows
class NestedForms::ApplicationController < ApplicationController
before_action :set_nested_form
protected
# Use callbacks to share common setup or constraints between actions.
def set_nested_form
@nested_form = @workflow.nested_forms.find(params[:nested_form_id])
end
end
end
# frozen_string_literal: true
module Workflows
class NestedForms::FieldsController < NestedForms::ApplicationController
before_action :set_field, only: %i[show edit update destroy]
def index
@fields = @nested_form.fields.all
end
def new
@field = @nested_form.fields.build
end
def edit; end
def create
@field = @nested_form.fields.build(field_params)
if @field.save!
redirect_to workflow_nested_form_fields_url(@workflow, @nested_form), notice: "Field was successfully created."
else
render :new
end
end
def update
if @field.update(field_params)
redirect_to workflow_nested_form_fields_url(@workflow, @nested_form), notice: "Field was successfully updated."
else
render :edit
end
end
def destroy
@field.destroy
redirect_to workflow_nested_form_fields_url(@workflow, @nested_form), notice: "Field was successfully destroyed."
end
private
# Use callbacks to share common setup or constraints between actions.
def set_field
@field = @nested_form.fields.find(params[:id])
end
# Only allow a trusted parameter "white list" through.
def field_params
params.fetch(:field, {}).permit(:name, :label, :hint, :accessibility, :type)
end
end
end
...@@ -12,4 +12,15 @@ module FieldsHelper ...@@ -12,4 +12,15 @@ module FieldsHelper
field.name == field_name field.name == field_name
end.first&.label end.first&.label
end end
def smart_form_fields_path(workflow, form)
case form
when Form
workflow_fields_path(workflow, form)
when NestedForm
workflow_nested_form_fields_path(workflow, form)
else
raise "Unknown form: #{form.class}"
end
end
end end
...@@ -5,7 +5,8 @@ class Field < ApplicationRecord ...@@ -5,7 +5,8 @@ class Field < ApplicationRecord
self.table_name = "fields" self.table_name = "fields"
belongs_to :form, class_name: "Form", foreign_key: "form_id", touch: true belongs_to :form, class_name: "MetalForm", foreign_key: "form_id", touch: true
belongs_to :workflow
validates :label, validates :label,
presence: true presence: true
...@@ -19,6 +20,13 @@ class Field < ApplicationRecord ...@@ -19,6 +20,13 @@ class Field < ApplicationRecord
-> (_) { "field_#{SecureRandom.hex(3)}" }, -> (_) { "field_#{SecureRandom.hex(3)}" },
allow_nil: false allow_nil: false
default_value_for :workflow_id,
-> (field) {
if field.has_attribute?(:workflow_id) || field.workflow
field&.form&.workflow_id
end
}, allow_nil: false
def self.type_key def self.type_key
model_name.name.split("::").last.underscore model_name.name.split("::").last.underscore
end end
...@@ -35,7 +43,7 @@ class Field < ApplicationRecord ...@@ -35,7 +43,7 @@ class Field < ApplicationRecord
validations.is_a?(FieldOptions) && validations.attributes.any? validations.is_a?(FieldOptions) && validations.attributes.any?
end end
def attach_choices? def attached_nested_form?
false false
end end
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
module Fields module Fields
%w[ %w[
text boolean decimal integer text boolean decimal integer
nested_form multiple_nested_form
].each do |type| ].each do |type|
require_dependency "fields/#{type}_field" require_dependency "fields/#{type}_field"
end end
......
# frozen_string_literal: true
module Fields
class MultipleNestedFormField < Field
has_one :nested_form, as: :attachable, dependent: :destroy
after_create do
build_nested_form(workflow: form.workflow).save!
end
serialize :validations, Validations::MultipleNestedFormField
serialize :options, Options::MultipleNestedFormField
def attached_nested_form?
true
end
def interpret_to(model, overrides: {})
check_model_validity!(model)
accessibility = overrides.fetch(:accessibility, self.accessibility)
return model if accessibility == :hidden
overrides[:name] = name
nested_model = nested_form.to_virtual_model(overrides: {_global: {accessibility: accessibility}})
model.nested_models[name] = nested_model
model.embeds_many name, anonymous_class: nested_model, validate: true
model.accepts_nested_attributes_for name, reject_if: :all_blank
if accessibility == :readonly
model.attr_readonly name
end
interpret_validations_to model, accessibility, overrides
interpret_extra_to model, accessibility, overrides
model
end
end
end
# frozen_string_literal: true
module Fields
class NestedFormField < Field
has_one :nested_form, as: :attachable, dependent: :destroy
after_create do
build_nested_form(workflow: form.workflow).save!
end
serialize :validations, Validations::NestedFormField
serialize :options, Options::NestedFormField
def attached_nested_form?
true
end
def interpret_to(model, overrides: {})
check_model_validity!(model)
accessibility = overrides.fetch(:accessibility, self.accessibility)
return model if accessibility == :hidden
nested_model = nested_form.to_virtual_model(overrides: {_global: {accessibility: accessibility}})
model.nested_models[name] = nested_model
model.embeds_one name, anonymous_class: nested_model, validate: true
model.accepts_nested_attributes_for name, reject_if: :all_blank
if accessibility == :readonly
model.attr_readonly name
end
interpret_validations_to model, accessibility, overrides
interpret_extra_to model, accessibility, overrides
model
end
end
end
# frozen_string_literal: true
module Fields::Options
class MultipleNestedFormField < FieldOptions
end
end
# frozen_string_literal: true
module Fields::Options
class NestedFormField < FieldOptions
end
end
# frozen_string_literal: true
module Fields::Validations
class MultipleNestedFormField < FieldOptions
prepend Concerns::Fields::Validations::Presence
prepend Concerns::Fields::Validations::Length
end
end
# frozen_string_literal: true
module Fields::Validations
class NestedFormField < FieldOptions
prepend Concerns::Fields::Validations::Presence
end
end
# frozen_string_literal: true # frozen_string_literal: true
class Form < ApplicationRecord class Form < MetalForm
include FormCore::Concerns::Models::Form
self.table_name = "forms"
belongs_to :workflow
has_many :fields, foreign_key: "form_id", dependent: :destroy
end end
# frozen_string_literal: true
class MetalForm < ApplicationRecord
include FormCore::Concerns::Models::Form
self.table_name = "forms"
belongs_to :workflow
has_many :fields, foreign_key: "form_id", dependent: :destroy
end
# frozen_string_literal: true
class NestedForm < MetalForm
belongs_to :attachable, polymorphic: true, touch: true
end
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
class Workflow < WorkflowCore::Workflow class Workflow < WorkflowCore::Workflow
has_one :form, dependent: :destroy has_one :form, dependent: :destroy
has_many :nested_forms
has_many :fields
after_create :auto_create_form! after_create :auto_create_form!
after_create :auto_create_start_place! after_create :auto_create_start_place!
......
# frozen_string_literal: true
module Fields
class CompositeFieldPresenter < FieldPresenter
def value
target&.send(@model.name)
end
def value_for_preview
target&.send(@model.name)
end
end
end
...@@ -4,7 +4,7 @@ class Fields::FieldPresenter < ApplicationPresenter ...@@ -4,7 +4,7 @@ class Fields::FieldPresenter < ApplicationPresenter
def required def required
@model.validations&.presence @model.validations&.presence
end end
alias_method :required?, :required alias required? required
def target def target
@options[:target] @options[:target]
...@@ -19,15 +19,16 @@ class Fields::FieldPresenter < ApplicationPresenter ...@@ -19,15 +19,16 @@ class Fields::FieldPresenter < ApplicationPresenter
end end
def access_readonly? def access_readonly?
target.class.attr_readonly?(@model.name) target.class.attr_readonly?(@model.name.to_s)
end end
def access_hidden? def access_hidden?
target.class.attribute_names.exclude?(@model.name.to_s) && target.class._reflections.keys.exclude?(@model.name.to_s) target.class.attribute_names.exclude?(@model.name.to_s) && target.class._reflections.keys.exclude?(@model.name.to_s)
end end
def access_read_and_write def access_read_and_write?
target.class.attribute_names.include?(@model.name.to_s) || target.class._reflections.keys.include?(@model.name.to_s) !access_readonly? &&
(target.class.attribute_names.include?(@model.name.to_s) || target.class._reflections.key?(@model.name.to_s))
end end
def id def id
......
# frozen_string_literal: true
module Fields
class MultipleNestedFormFieldPresenter < CompositeFieldPresenter
def multiple_nested_form?
true
end
end
end
# frozen_string_literal: true
module Fields
class NestedFormFieldPresenter < CompositeFieldPresenter
def nested_form_field?
true
end
end
end
...@@ -5,6 +5,6 @@ module Fields ...@@ -5,6 +5,6 @@ module Fields
def multiline def multiline
@model.options.multiline @model.options.multiline
end end
alias_method :multiline?, :multiline alias multiline? multiline
end end
end end
<div class="field nested_form_field">
<h2 class="label"><%= field.label %></h2>
<%= tag.div id: field.name, class: "collection" do %>
<%= f.fields_for field.name do |ff| %>
<%= render "_form_core/fields/nested_form", f: ff, field: field, form: field.nested_form %>
<% end %>
<div class="links">
<% if field.access_read_and_write? %>
<%= link_to_add_association "Add", f, field.name,
class: "button is-small",
partial: "_form_core/fields/nested_form",
render_options: {
locals: {field: field, form: field.nested_form}
} %>
<% end %>
<% if field.hint.present? %>
<p class="help"><%= field.hint %></p>
<% end %>
</div>
<% end %>
</div>
<div class="nested_form">
<% if f.object.errors.any? %>
<article class="message is-danger">
<div class="message-header">
<p>
<%= pluralize(f.object.errors.count, "error") %> prohibited this form from being submitted:
</p>
</div>
<div class="message-body content">
<ul>
<% f.object.errors.messages.each do |name, messages| %>
<% messages.each do |message| %>
<li>
<%= "#{form.fields.select { |field| field.name == name}.first&.label } #{message}" %>
</li>
<% end %>
<% end %>
</ul>
</div>
</article>
<% end %>
<% form.fields.each do |field| %>
<% field = present(field, target: f.object) %>
<% unless field.access_hidden? %>
<%= render "_form_core/fields/#{field.type_key}", f: f, field: field %>
<% end %>
<% end %>
<% if field.access_read_and_write? %>
<div class="field is-grouped">
<div class="control">
<%= link_to_remove_association "Remove", f, class: "button is-small is-danger", wrapper_class: "nested_form" %>
</div>
</div>
<% end %>
</div>
<div class="field nested_form_field">
<h2 class="label"><%= field.label %></h2>
<%= tag.div id: field.name, class: "collection" do %>
<%= f.fields_for field.name do |ff| %>
<%= render "_form_core/fields/nested_form", f: ff, field: field, form: field.nested_form %>
<% end %>
<div class="links">
<% if field.access_read_and_write? %>
<%= link_to_add_association "Add", f, field.name,
force_non_association_create: true,
class: "button is-small",
partial: "_form_core/fields/nested_form",
render_options: {
locals: {field: field, form: field.nested_form}
} %>
<% end %>
<% if field.hint.present? %>
<p class="help"><%= field.hint %></p>
<% end %>
</div>
<% end %>
</div>
<% if field.access_read_and_write? %>
<script>
document.addEventListener("turbolinks:load", function() {
if($('<%= "##{field.name}" %> > .nested_form').length > 0) {
$('<%= "##{field.name}" %> .links a.add_fields').hide();
}
$('<%= "##{field.name}" %>')
.on('cocoon:before-insert', function() {
$('<%= "##{field.name}" %> .links a.add_fields').hide();
})
.on("cocoon:before-remove", function() {
$('<%= "##{field.name}" %> .links a.add_fields').show();
});
});
</script>
<% end %>
<% form.fields.map { |field| present(field, target: instance) }.each do |field| %> <% form.fields.map { |field| present(field, target: instance) }.each do |field| %>
<% if field.nested_form_field? %>
<% next unless field.value_for_preview %>
<p><%= field.label %>:</p>
<%= render "_form_core/preview/nested_form", form: field.nested_form, instance: field.value_for_preview %>
<% elsif field.multiple_nested_form? %>
<% next if field.value_for_preview.empty? %>
<p><%= field.label %>:</p>
<%= field.value_for_preview.map do |nested_instance| %>
<% render "_form_core/preview/nested_form", form: field.nested_form, instance: nested_instance %>
<% end.join("<hr>").html_safe %>
<% else %>
<p><%= field.label %>: <%= field.value_for_preview %></p> <p><%= field.label %>: <%= field.value_for_preview %></p>
<% end %>
<% end %> <% end %>
<% options ||= {} %> <% options ||= {} %>
<% back_url ||= url_for(:back) %>
<%= form_with(model: instance, **options) do |f| %> <%= form_with(model: instance, **options) do |f| %>
<% if instance.errors.any? %> <% if instance.errors.any? %>
<article class="message is-danger"> <article class="message is-danger">
...@@ -31,7 +33,7 @@ ...@@ -31,7 +33,7 @@
<%= f.submit "Submit", class: "button is-primary" %> <%= f.submit "Submit", class: "button is-primary" %>
</div> </div>
<div class="control"> <div class="control">
<%= link_to "Back", url_for(:back), class: "button is-link" %> <%= link_to "Back", back_url, class: "button is-link" %>
</div> </div>
</div> </div>
<% end %> <% end %>
...@@ -25,6 +25,9 @@ ...@@ -25,6 +25,9 @@
<% if field.options_configurable? %> <% if field.options_configurable? %>
<%= link_to "Options", edit_workflow_field_options_path(@workflow, field) %> | <%= link_to "Options", edit_workflow_field_options_path(@workflow, field) %> |
<% end %> <% end %>
<% if field.attached_nested_form? %>
<%= link_to "Fields", workflow_nested_form_fields_url(@workflow, field.nested_form) %> |
<% end %>
<%= link_to "Edit", edit_workflow_field_path(@workflow, field) %> | <%= link_to "Edit", edit_workflow_field_path(@workflow, field) %> |
<%= link_to "Destroy", workflow_field_path(@workflow, field), method: :delete, data: {confirm: "Are you sure?"} %> <%= link_to "Destroy", workflow_field_path(@workflow, field), method: :delete, data: {confirm: "Are you sure?"} %>
</td> </td>
......
<%= form_with(model: [workflow, form, field.becomes(Field)], scope: :field, local: true) do |f| %>
<% if field.errors.any? %>
<article class="message is-danger">
<div class="message-header">
<p>
<%= pluralize(field.errors.count, "error") %> prohibited this form from being saved:
</p>
</div>
<div class="message-body content">
<ul>
<% field.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
</article>
<% end %>
<div class="field">
<%= f.label :name, class: "label" %>
<div class="control">
<%= f.text_field :name, class: "input", placeholder: "Name" %>
</div>
</div>
<div class="field">
<%= f.label :label, class: "label" %>
<div class="control">
<%= f.text_field :label, class: "input", placeholder: "Label" %>
</div>
</div>
<div class="field">
<%= f.label :hint, class: "label" %>
<div class="control">
<%= f.text_field :hint, class: "input", placeholder: "Hint" %>
</div>
</div>
<div class="field">
<%= f.label :type, class: "label" %>
<div class="control">
<span class="select">
<%= f.select :type, options_for_field_types(selected: field.class.to_s) %>
</span>
</div>
</div>
<div class="field">
<div class="control">
<% Field.accessibilities.each do |k, _| %>
<label class="radio">
<%= f.radio_button :accessibility, k %>
<%= Field.human_enum_value :accessibility, k %>
</label>
<% end %>
</div>
</div>
<div class="field is-grouped">
<div class="control">
<%= f.submit class: "button is-primary" %>
</div>
<div class="control">
<%= link_to "Back", url_for(:back), class: "button is-link" %>
</div>
</div>
<% end %>
<div class="section">
<div class="container">
<p class="title is-1">Editing field</p>
<%= render "form", workflow: @workflow, form: @nested_form, field: @field %>
</div>
</div>
<div class="section">
<div class="container">
<p content="content">
<%= link_to "New field", new_workflow_nested_form_field_path(@workflow, @nested_form), class: "button is-primary" %>
<%= link_to "Back", smart_form_fields_path(@workflow, @nested_form.attachable.form), class: "button is-link" %>
</p>
<table class="table is-fullwidth">
<thead>
<tr>
<th>Name</th>
<th>Label</th>
<th>Type</th>
<th>Accessibility</th>
<th></th>
</tr>
</thead>
<tbody>
<% @fields.each do |field| %>
<tr>
<td><%= field.name %></td>
<td><%= field.label %></td>
<td><%= field.class.model_name.human %></td>
<td><%= Field.human_enum_value(:accessibility, field.accessibility) %></td>
<td>
<% if field.validations_configurable? %>
<%= link_to "Validations", edit_workflow_field_validations_path(@workflow, field) %> |
<% end %>
<% if field.options_configurable? %>
<%= link_to "Options", edit_workflow_field_options_path(@workflow, field) %> |
<% end %>
<% if field.attached_nested_form? %>
<%= link_to "Fields", workflow_nested_form_fields_url(@workflow, field.nested_form) %> |
<% end %>
<%= link_to "Edit", edit_workflow_nested_form_field_path(@workflow, @nested_form, field) %> |
<%= link_to "Destroy", workflow_nested_form_field_path(@workflow, @nested_form, field), method: :delete, data: {confirm: "Are you sure?"} %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
<div class="section">
<div class="container">
<p class="title is-1">New field</p>
<%= render "form", workflow: @workflow, form: @nested_form, field: @field %>
</div>
</div>
...@@ -13,6 +13,11 @@ Rails.application.routes.draw do ...@@ -13,6 +13,11 @@ Rails.application.routes.draw do
resource :options, only: %i[edit update] resource :options, only: %i[edit update]
end end
end end
resources :nested_forms, only: %i[] do
scope module: :nested_forms do
resources :fields, except: %i[show]
end
end
resources :transitions, except: %i[show] do resources :transitions, except: %i[show] do
scope module: :transitions do scope module: :transitions do
resource :options, only: %i[edit update] resource :options, only: %i[edit update]
......
# frozen_string_literal: true
class AddAttachableToForms < ActiveRecord::Migration[5.2]
def change
change_table :forms do |t|
t.references :attachable, polymorphic: true, index: true
end
end
end
# frozen_string_literal: true
class AddWorkflowIdToFields < ActiveRecord::Migration[5.2]
def change
change_table :fields do |t|
t.references :workflow, foreign_key: true
end
end
end
...@@ -24,15 +24,20 @@ ActiveRecord::Schema.define(version: 2018_09_22_095933) do ...@@ -24,15 +24,20 @@ ActiveRecord::Schema.define(version: 2018_09_22_095933) do
t.string "label", default: "" t.string "label", default: ""
t.string "hint", default: "" t.string "hint", default: ""
t.integer "position" t.integer "position"
t.integer "workflow_id"
t.index ["form_id"], name: "index_fields_on_form_id" t.index ["form_id"], name: "index_fields_on_form_id"
t.index ["type"], name: "index_fields_on_type" t.index ["type"], name: "index_fields_on_type"
t.index ["workflow_id"], name: "index_fields_on_workflow_id"
end end
create_table "forms", force: :cascade do |t| create_table "forms", force: :cascade do |t|
t.string "type", null: false t.string "type", null: false
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.string "attachable_type"
t.integer "attachable_id"
t.integer "workflow_id" t.integer "workflow_id"
t.index ["attachable_type", "attachable_id"], name: "index_forms_on_attachable_type_and_attachable_id"
t.index ["type"], name: "index_forms_on_type" t.index ["type"], name: "index_forms_on_type"
t.index ["workflow_id"], name: "index_forms_on_workflow_id" t.index ["workflow_id"], name: "index_forms_on_workflow_id"
end end
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment