Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add scaffold generator #137

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions lib/generators/cable_ready/scaffold/USAGE
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
Description:
Creates a CableReady-driven resource scaffold with in-page editing
via UJS remote links/forms and `render operations: cable_car`.

Scaffolds the entire resource, from model and migration to controller and
views, along with a full test suite. It will also ensure all javascript
dependencies for in-page editing are set up (Stimulus, RailsUJS, and a
custom Stimulus controller).

Pass the name of the model (in singular form), either CamelCased or
under_scored, as the first argument, and an optional list of attribute
pairs.

Attributes are field arguments specifying the model's attributes. You can
optionally pass the type and an index to each field. For instance:
'title body:text tracking_id:integer:uniq' will generate a title field of
string type, a body with text type and a tracking_id as an integer with an
unique index. "index" could also be given instead of "uniq" if one desires
a non unique index.

As a special case, specifying 'password:digest' will generate a
password_digest field of string type, and configure your generated model,
controller, views, and test suite for use with Active Model
has_secure_password (assuming they are using Rails defaults).

Timestamps are added by default, so you don't have to specify them by hand
as 'created_at:datetime updated_at:datetime'.

You don't have to think up every attribute up front, but it helps to
sketch out a few so you can start working with the resource immediately.

For example, 'scaffold post title body:text published:boolean' gives
you a model with those three attributes, a controller that handles
the create/show/update/destroy, forms to create and edit your posts, and
an index that lists them all, as well as a resources :posts declaration
in config/routes.rb.

If you want to remove all the generated files, run
'bin/rails destroy cable_ready:scaffold ModelName'.

Examples:
`bin/rails generate cable_ready:scaffold post`
`bin/rails generate cable_ready:scaffold post title:string body:text published:boolean`
`bin/rails generate cable_ready:scaffold purchase amount:decimal tracking_id:integer:uniq`
`bin/rails generate cable_ready:scaffold user email:uniq password:digest`
128 changes: 128 additions & 0 deletions lib/generators/cable_ready/scaffold/scaffold_generator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# frozen_string_literal: true

require "rails/generators/rails/resource/resource_generator"
require "rake"

class CableReady::ScaffoldGenerator < Rails::Generators::ResourceGenerator
include Rails::Generators::ResourceHelpers

source_root File.expand_path("templates", __dir__)
remove_hook_for :resource_controller
remove_class_option :actions

check_class_collision suffix: "Controller"

class_option :skip_system_tests, type: :boolean, default: false, desc: "Skip system test files"

def create_controller_files
template "controller.rb",
File.join("app/controllers", controller_class_path, "#{controller_file_name}_controller.rb")
end

def create_view_root
empty_directory File.join("app/views", controller_file_path)
end

# TODO: ERB only for now, add alternate template engine support
def copy_view_files
available_views.each do |view|
target_view = if view == "_resource"
"_#{controller_file_path.singularize}"
else
view
end

source_filename = File.join("erb", (view + ".html.erb"))
target_filename = target_view + ".html.erb"
template source_filename, File.join("app/views", controller_file_path, target_filename)
end
end

def install_scripts
if defined?(Webpacker)
main_folder = Webpacker.config.source_path.to_s.gsub("#{Rails.root}/", "")
unless File.exist? File.join("app", "javascript", "controllers")
say "Adding Stimulus"
system "bin/rake webpacker:install:stimulus"
end
else
main_folder = File.join("app", "assets", "javascripts")
# TODO: automate?
say "Please be sure Stimulus is installed. See https://github.com/hotwired/stimulus-rails"
end

filepath = [
"#{main_folder}/packs/application.js",
"#{main_folder}/packs/application.ts"
]
.select { |path| File.exist?(path) }
.map { |path| Rails.root.join(path) }
.first

lines = File.open(filepath, "r") { |f| f.readlines }

unless lines.find { |line| line.start_with?("import Rails") }
say "Adding @rails/ujs via yarn"
system "bin/yarn add @rails/ujs"
append_file filepath, "\nimport Rails from '@rails/ujs'\nRails.start()\n"
end

template "cable_car_controller.js", File.join(main_folder, "controllers", "cable_car_controller.js")
end

def create_test_files
unless options[:skip_system_tests]
template "system_test.rb", File.join("test/system", class_path, "#{file_name.pluralize}_test.rb")
end
end

private

def available_views
%w[index show _form _resource]
end

def permitted_params
attachments, others = attributes_names.partition { |name| attachments?(name) }
params = others.map { |name| ":#{name}" }
params += attachments.map { |name| "#{name}: []" }
params.join(", ")
end

def attachments?(name)
attribute = attributes.find { |attr| attr.name == name }
attribute&.attachments?
end

# For system tests
def attributes_hash
return {} if attributes_names.empty?

attributes_names.map do |name|
if %w[password password_confirmation].include?(name) && attributes.any?(&:password_digest?)
[name.to_s, "'secret'"]
elsif !virtual?(name)
[name.to_s, "@#{singular_table_name}.#{name}"]
end
end.compact.sort.to_h
end

def boolean?(name)
attribute = attributes.find { |attr| attr.name == name }
attribute&.type == :boolean
end

def virtual?(name)
attribute = attributes.find { |attr| attr.name == name }
attribute&.virtual?
end

def fixture_name
@fixture_name ||=
if mountable_engine?
(namespace_dirs + [table_name]).join("_")
else
table_name
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Controller } from 'stimulus'
import CableReady from 'cable_ready'

export default class extends Controller {
connect() {
console.log("Connect!")
this.boundPerform = this.perform.bind(this)
this.element.addEventListener("ajax:success", this.boundPerform)
}

perform(event) {
CableReady.perform(event.detail[0])
}

disconnect() {
this.element.removeEventListener("ajax:success", this.boundPerform)
}
}
85 changes: 85 additions & 0 deletions lib/generators/cable_ready/scaffold/templates/controller.rb.tt
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<% if namespaced? -%>
require_dependency "<%= namespaced_path %>/application_controller"

<% end -%>
<% module_namespacing do -%>
class <%= controller_class_name %>Controller < ApplicationController
include CableReady::Broadcaster
before_action :set_<%= singular_table_name %>, only: [:show, :edit, :update, :destroy]

# GET <%= route_url %>
def index
@<%= plural_table_name %> = <%= orm_class.all(class_name) %>
respond_to do |format|
format.html { render }
format.json { render operations: cable_car.inner_html(@<%= plural_table_name %>, html: self.class.render(@<%= plural_table_name %>)) }
end
end

# GET <%= route_url %>/1
def show
end

# GET <%= route_url %>/new
def new
@<%= singular_table_name %> = <%= orm_class.build(class_name) %>
render operations: cable_car.append(selector: "#<%= plural_table_name %>", html: render_form(@<%= singular_table_name %>))
end

# GET <%= route_url %>/1/edit
def edit
render operations: cable_car.outer_html(selector: @<%= singular_table_name %>, html: render_form(@<%= singular_table_name %>))
end

# POST <%= route_url %>
def create
@<%= singular_table_name %> = <%= orm_class.build(class_name, "#{singular_table_name}_params") %>

if @<%= orm_instance.save %>
render operations: cable_car
.append(selector: "#<%= plural_table_name %>", html: render_<%= singular_table_name %>(@<%= singular_table_name %>))
.remove(selector: "#new_<%= singular_table_name %>")
else
render operations: cable_car.outer_html(selector: @<%= singular_table_name %>, html: render_form(@<%= singular_table_name %>))
end
end

# PATCH/PUT <%= route_url %>/1
def update
if @<%= orm_instance.update("#{singular_table_name}_params") %>
render operations: cable_car.outer_html(selector: @<%= singular_table_name %>, html: render_<%= singular_table_name %>(@<%= singular_table_name %>))
else
render operations: cable_car.outer_html(selector: @<%= singular_table_name %>, html: render_form(@<%= singular_table_name %>))
end
end

# DELETE <%= route_url %>/1
def destroy
@<%= orm_instance.destroy %>
render operations: cable_car.remove(selector: @<%= singular_table_name %>)
end

private
# Use callbacks to share common setup or constraints between actions.
def set_<%= singular_table_name %>
@<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %>
end

# Only allow a list of trusted parameters through.
def <%= "#{singular_table_name}_params" %>
<%- if attributes_names.empty? -%>
params.fetch(:<%= singular_table_name %>, {})
<%- else -%>
params.require(:<%= singular_table_name %>).permit(<%= permitted_params %>)
<%- end -%>
end

def render_<%= singular_table_name %> <%= singular_table_name %>
self.class.render(<%= singular_table_name %>)
end

def render_form <%= singular_table_name %>
self.class.render(partial: "form", locals: {<%= singular_table_name %>: <%= singular_table_name %>})
end
end
<% end -%>
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<%%= form_with(model: <%= model_resource_name %>, data: {remote: true, type: "json"}, id: dom_id(<%= singular_table_name %>), html: {style: "margin-bottom: 36px;"}) do |form| %>
<%% if <%= singular_table_name %>.errors.any? %>
<div>
<h2><%%= pluralize(<%= singular_table_name %>.errors.count, "error") %> prohibited this <%= singular_table_name %> from being saved:</h2>

<ul>
<%% <%= singular_table_name %>.errors.each do |error| %>
<li><%%= error.full_message %></li>
<%% end %>
</ul>
</div>
<%% end %>

<% attributes.each do |attribute| -%>
<div>
<% if attribute.password_digest? -%>
<%%= form.label :password %><br>
<%%= form.password_field :password %>
</div>

<div>
<%%= form.label :password_confirmation %><br>
<%%= form.password_field :password_confirmation %>
<% elsif attribute.attachments? -%>
<%%= form.label :<%= attribute.column_name %> %><br>
<%%= form.<%= attribute.field_type %> :<%= attribute.column_name %>, multiple: true %>
<% else -%>
<%%= form.label :<%= attribute.column_name %> %><br>
<%%= form.<%= attribute.field_type %> :<%= attribute.column_name %> %>
<% end -%>
</div>

<% end -%>
<div style="margin-top: 12px">
<%%= form.submit %>
<%%= link_to "Cancel", <%= plural_table_name %>_path, data: {remote: true, type: "json"}, style: "margin-left: 8px;" %>
</div>
<%% end %>
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<div id="<%%= dom_id(<%= singular_table_name %>) %>" style="margin-bottom: 36px;">
<dl>
<% attributes.reject(&:password_digest?).each do |attribute| -%>
<dt><%= attribute.human_name %></dt>
<dd><%%= <%= singular_table_name %>.<%= attribute.column_name %> %></dd>
<% end -%>
</dl>

<div>
<td><%%= link_to '[show]', <%= model_resource_name %> %></td>
<td><%%= link_to '[edit]', edit_<%= singular_route_name %>_path(<%= singular_table_name %>), data: {remote: true, type: "json"}%></td>
<td><%%= link_to '[destroy]', <%= model_resource_name %>, method: :delete, data: { confirm: 'Are you sure?', remote: true, type: "json" } %></td>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<div data-controller="cable-car">
<h1><%= plural_table_name.titleize %></h1>

<div id="<%= plural_table_name %>">
<%%= render @<%= plural_table_name %> %>
</div>

<%%= link_to 'New <%= singular_table_name.titleize %>', new_<%= singular_route_name %>_path, data: {remote: true, type: "json"} %>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<div data-controller="cable-car">
<%%= render @<%= singular_table_name %> %>

<%%= link_to 'Back', <%= index_helper %>_path %>
</div>
Loading