thecore_generators 3.2.0 → 3.6.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 +4 -4
- data/README.md +160 -2
- data/lib/generators/thecore/action_companion.rb +116 -0
- data/lib/generators/thecore/association_wiring.rb +217 -0
- data/lib/generators/thecore/atom_aware.rb +14 -3
- data/lib/generators/thecore/companion_files.rb +142 -0
- data/lib/generators/thecore/member_action/member_action_generator.rb +61 -0
- data/lib/generators/thecore/member_action/templates/action.html.erb.tt +16 -0
- data/lib/generators/thecore/member_action/templates/action.js.tt +44 -0
- data/lib/generators/thecore/member_action/templates/action.rb.tt +25 -0
- data/lib/generators/thecore/member_action/templates/action.scss.tt +38 -0
- data/lib/generators/thecore/migration/migration_generator.rb +18 -7
- data/lib/generators/thecore/model/model_generator.rb +14 -0
- data/lib/generators/thecore/root_action/root_action_generator.rb +80 -0
- data/lib/generators/thecore/root_action/templates/action.html.erb.tt +13 -0
- data/lib/generators/thecore/root_action/templates/action.js.tt +42 -0
- data/lib/generators/thecore/root_action/templates/action.rb.tt +33 -0
- data/lib/generators/thecore/root_action/templates/action.scss.tt +38 -0
- data/lib/generators/thecore/workspace_context.rb +48 -0
- data/lib/tasks/thecore_generators_tasks.rake +33 -0
- data/lib/thecore_generators/check_practices.rb +392 -0
- data/lib/thecore_generators/railtie.rb +4 -0
- data/lib/thecore_generators/version.rb +1 -1
- metadata +16 -1
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
require "yaml"
|
|
2
|
+
|
|
3
|
+
module Thecore
|
|
4
|
+
module Generators
|
|
5
|
+
# Shared companion-file behavior for Thecore's own custom-action
|
|
6
|
+
# generators: ensuring config/initializers/after_initialize.rb and
|
|
7
|
+
# assets.rb exist and carry the right require/precompile line, and
|
|
8
|
+
# writing the RailsAdmin action's locale entries. Introduced by
|
|
9
|
+
# RootActionGenerator (thecore_generators#11) and reused as-is by
|
|
10
|
+
# MemberActionGenerator (thecore_generators#12).
|
|
11
|
+
#
|
|
12
|
+
# Faithful Ruby port of the corresponding parts of thecore_code_extension's
|
|
13
|
+
# addRootAction.js/addMemberAction.js: the after_initialize.rb/assets.rb
|
|
14
|
+
# ensure-and-append logic, and the locale-entry merge behavior — broadened
|
|
15
|
+
# per thecore_generators#11's acceptance criteria to write into every
|
|
16
|
+
# *.yml file already present under the locales directory, not just
|
|
17
|
+
# en.yml/it.yml (the JS original only ever touched those two).
|
|
18
|
+
#
|
|
19
|
+
# Deliberately duplicates (rather than shares) the after_initialize.rb
|
|
20
|
+
# skeleton Thecore::Generators::AssociationWiring already writes — same
|
|
21
|
+
# structure/anchor, so the two compose fine regardless of which one
|
|
22
|
+
# happens to create the file first, but kept separate to avoid touching
|
|
23
|
+
# already-shipped Phase 1 code for this ticket.
|
|
24
|
+
module CompanionFiles
|
|
25
|
+
AFTER_INITIALIZE_TEMPLATE = <<~RUBY.freeze
|
|
26
|
+
Rails.application.configure do
|
|
27
|
+
config.after_initialize do
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
RUBY
|
|
31
|
+
|
|
32
|
+
ASSETS_TEMPLATE = <<~RUBY.freeze
|
|
33
|
+
# PLEASE, uncomment if needed.
|
|
34
|
+
# For Example: in the case there's a root action called tcp_debug, add the following lines to include css and javascripts for auto loading:
|
|
35
|
+
# Rails.application.config.assets.precompile += %w(
|
|
36
|
+
# main_tcp_debug.js
|
|
37
|
+
# main_tcp_debug.css
|
|
38
|
+
# )
|
|
39
|
+
RUBY
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
# Renders the shared view/JS/SCSS companion trio for a RailsAdmin
|
|
44
|
+
# custom action (root or member) into the workspace's fixed
|
|
45
|
+
# app/views/rails_admin/main, app/assets/javascripts/rails_admin/actions,
|
|
46
|
+
# and app/assets/stylesheets/rails_admin/actions directories — the same
|
|
47
|
+
# relative paths regardless of ATOM vs host-app context (only the
|
|
48
|
+
# action's own controller-config file, handled by the including
|
|
49
|
+
# generator, is placed differently: lib/root_actions vs
|
|
50
|
+
# config/root_actions, and similarly for member actions).
|
|
51
|
+
# Requires "action.html.erb.tt"/"action.js.tt"/"action.scss.tt" to be
|
|
52
|
+
# present in the including generator's own source_paths. Takes no
|
|
53
|
+
# argument deliberately: the template bodies themselves read the
|
|
54
|
+
# including generator's own `file_name` (via NamedBase) through the ERB
|
|
55
|
+
# binding `template` evaluates them in, so a separate action-name
|
|
56
|
+
# argument here would only rename the destination files while the
|
|
57
|
+
# content inside kept using `file_name` — a silent name/content
|
|
58
|
+
# mismatch. Callers needing a different action name for content must
|
|
59
|
+
# get there via their own `file_name`, not a parameter to this method.
|
|
60
|
+
def render_view_js_scss_companions!
|
|
61
|
+
template "action.html.erb.tt", File.join("app/views/rails_admin/main", "#{file_name}.html.erb")
|
|
62
|
+
template "action.js.tt", File.join("app/assets/javascripts/rails_admin/actions", "#{file_name}.js")
|
|
63
|
+
template "action.scss.tt", File.join("app/assets/stylesheets/rails_admin/actions", "#{file_name}.scss")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Ensures config/initializers/after_initialize.rb exists (creating it
|
|
67
|
+
# from the skeleton above if absent) and that `require_line` is present
|
|
68
|
+
# inside its `config.after_initialize do ... end` block — idempotently.
|
|
69
|
+
def ensure_after_initialize_require!(require_line)
|
|
70
|
+
path = "config/initializers/after_initialize.rb"
|
|
71
|
+
full_path = File.join(destination_root, path)
|
|
72
|
+
|
|
73
|
+
create_file(path, AFTER_INITIALIZE_TEMPLATE) unless File.exist?(full_path)
|
|
74
|
+
|
|
75
|
+
content = File.read(full_path)
|
|
76
|
+
if content.include?(require_line)
|
|
77
|
+
say_status :skip, "#{path} already requires it", :blue
|
|
78
|
+
else
|
|
79
|
+
insert_into_file(path, " #{require_line}\n", after: /config\.after_initialize do\n/)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Ensures config/initializers/assets.rb exists (creating it from the
|
|
84
|
+
# skeleton above if absent) and that `precompile_line` is appended to
|
|
85
|
+
# it — idempotently.
|
|
86
|
+
def ensure_assets_precompile_line!(precompile_line)
|
|
87
|
+
path = "config/initializers/assets.rb"
|
|
88
|
+
full_path = File.join(destination_root, path)
|
|
89
|
+
|
|
90
|
+
create_file(path, ASSETS_TEMPLATE) unless File.exist?(full_path)
|
|
91
|
+
|
|
92
|
+
content = File.read(full_path)
|
|
93
|
+
if content.include?(precompile_line)
|
|
94
|
+
say_status :skip, "#{path} already has the precompile line", :blue
|
|
95
|
+
else
|
|
96
|
+
append_to_file(path, "\n#{precompile_line}\n")
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Writes the RailsAdmin action's menu/title/breadcrumb locale entry
|
|
101
|
+
# (all three set to `title`, matching addRootAction.js/
|
|
102
|
+
# addMemberAction.js's mergeYaml behavior) under `admin.actions.<key>`
|
|
103
|
+
# into every *.yml file already present under config/locales — not just
|
|
104
|
+
# en.yml/it.yml, per thecore_generators#11's acceptance criteria. When
|
|
105
|
+
# the locales directory has no *.yml file yet, en.yml and it.yml are
|
|
106
|
+
# created first and then updated the same way.
|
|
107
|
+
def write_action_locale_entries!(key, title)
|
|
108
|
+
locales_dir = File.join(destination_root, "config", "locales")
|
|
109
|
+
existing = Dir.exist?(locales_dir) ? Dir.children(locales_dir).select { |f| f.end_with?(".yml") } : []
|
|
110
|
+
|
|
111
|
+
existing = %w[en.yml it.yml] if existing.empty?
|
|
112
|
+
|
|
113
|
+
existing.sort.each do |file|
|
|
114
|
+
rel_path = File.join("config", "locales", file)
|
|
115
|
+
full_path = File.join(destination_root, rel_path)
|
|
116
|
+
default_lang = File.basename(file, ".yml")
|
|
117
|
+
|
|
118
|
+
create_file(rel_path, "#{default_lang}:\n") unless File.exist?(full_path)
|
|
119
|
+
merge_action_locale_entry!(rel_path, default_lang, key, title)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# `lang` is the file's own single top-level key when it already has
|
|
124
|
+
# exactly one (the locale code, e.g. "en" in a Rails/Devise-style
|
|
125
|
+
# "devise.en.yml" whose filename does not equal its locale code) —
|
|
126
|
+
# falling back to `default_lang` (derived from the filename) only for
|
|
127
|
+
# an empty/freshly-created file, where there is nothing yet to read the
|
|
128
|
+
# real locale code from.
|
|
129
|
+
def merge_action_locale_entry!(rel_path, default_lang, key, title)
|
|
130
|
+
full_path = File.join(destination_root, rel_path)
|
|
131
|
+
data = YAML.load_file(full_path) || {}
|
|
132
|
+
lang = data.size == 1 ? data.keys.first : default_lang
|
|
133
|
+
data[lang] = {} unless data[lang].is_a?(Hash)
|
|
134
|
+
data[lang]["admin"] = {} unless data[lang]["admin"].is_a?(Hash)
|
|
135
|
+
data[lang]["admin"]["actions"] = {} unless data[lang]["admin"]["actions"].is_a?(Hash)
|
|
136
|
+
data[lang]["admin"]["actions"][key] = { "menu" => title, "title" => title, "breadcrumb" => title }
|
|
137
|
+
|
|
138
|
+
create_file(rel_path, YAML.dump(data), force: true, verbose: false)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
require "rails/generators/named_base"
|
|
2
|
+
require "generators/thecore/atom_aware"
|
|
3
|
+
require "generators/thecore/companion_files"
|
|
4
|
+
require "generators/thecore/action_companion"
|
|
5
|
+
|
|
6
|
+
module Thecore
|
|
7
|
+
module Generators
|
|
8
|
+
# `rails generate thecore:member_action NAME` — a Ruby port of
|
|
9
|
+
# thecore_code_extension's addMemberAction.js (thecore_generators#12),
|
|
10
|
+
# the Member Action counterpart to RootActionGenerator
|
|
11
|
+
# (thecore_generators#11). Shares the entire generator step sequence and
|
|
12
|
+
# placement/naming logic with it via Thecore::Generators::ActionCompanion
|
|
13
|
+
# (see that module and RootActionGenerator's own comment) — only this
|
|
14
|
+
# class's own `templates/action.rb.tt`/`action.html.erb.tt`/
|
|
15
|
+
# `action.js.tt` are Member-specific: the RailsAdmin `:member` action
|
|
16
|
+
# type and its XHR + form PATCH example, matching what
|
|
17
|
+
# `addMemberAction.js` produces today (not unified with Root's fetch +
|
|
18
|
+
# ActionCable-broadcast template).
|
|
19
|
+
#
|
|
20
|
+
# Discovered automatically by Rails::Generators' own namespace-by-path
|
|
21
|
+
# convention (`generators/thecore/member_action/member_action_generator.rb`
|
|
22
|
+
# → "thecore:member_action") — no Railtie registration needed, same as
|
|
23
|
+
# RootActionGenerator.
|
|
24
|
+
class MemberActionGenerator < Rails::Generators::NamedBase
|
|
25
|
+
include Thecore::Generators::AtomAware
|
|
26
|
+
include Thecore::Generators::CompanionFiles
|
|
27
|
+
include Thecore::Generators::ActionCompanion
|
|
28
|
+
|
|
29
|
+
action_kind "member_action"
|
|
30
|
+
|
|
31
|
+
source_root File.expand_path("templates", __dir__)
|
|
32
|
+
|
|
33
|
+
# Thin task methods, required on each class directly (see
|
|
34
|
+
# ActionCompanion's own comment for why) - each delegates to shared
|
|
35
|
+
# private logic there.
|
|
36
|
+
def validate_action_name!
|
|
37
|
+
validate_action_name_for_kind!
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def create_action_file
|
|
41
|
+
template "action.rb.tt", action_file_path
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def create_view_js_scss_companions
|
|
45
|
+
render_view_js_scss_companions!
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def add_after_initialize_require
|
|
49
|
+
ensure_after_initialize_require!(require_line)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def add_assets_precompile_line
|
|
53
|
+
ensure_assets_precompile_line!(assets_precompile_line)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def add_locale_entries
|
|
57
|
+
write_action_locale_entries!(file_name, title_case_name)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<%%= stylesheet_link_tag 'rails_admin/actions/<%= file_name %>' %>
|
|
2
|
+
<%%= form_with(url: <%= file_name %>_path, html: { method: :patch }, class: "main", id: "<%= file_name %>-form") do |f| %>
|
|
3
|
+
<div class="form-actions row justify-content-end my-3">
|
|
4
|
+
<div class="col-sm-10">
|
|
5
|
+
<input name="return_to" type="hidden" value="<%%=edit_path(@abstract_model, @object.id)%>">
|
|
6
|
+
<button class="btn btn-primary" data-disable-with="Save" name="_save" type="submit">
|
|
7
|
+
<i class="fas fa-check"></i>
|
|
8
|
+
Test
|
|
9
|
+
</button>
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
<%% end %>
|
|
13
|
+
<!-- The button to test the xhr get request -->
|
|
14
|
+
<button id="<%= file_name %>-id" class="btn btn-primary" data-url="<%%= rails_admin.<%= file_name %>_path %>">Test <%= file_name %></button>
|
|
15
|
+
<div id="<%= file_name %>-response"></div>
|
|
16
|
+
<%%= javascript_include_tag "rails_admin/actions/<%= file_name %>" %>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
var <%= action_name_camel_case %>Cable = null;
|
|
2
|
+
// If the <%= action_name_camel_case %>Function is already defined, then don't redefine it and don't attach it to the eventListener
|
|
3
|
+
if (typeof <%= action_name_camel_case %>Function !== 'function') {
|
|
4
|
+
function <%= action_name_camel_case %>Function(event) {
|
|
5
|
+
console.log('Hello from <%= file_name %>', event);
|
|
6
|
+
// Action Cable WebSocket connection only if <%= action_name_camel_case %>Cable is not already defined and valid
|
|
7
|
+
if (typeof <%= action_name_camel_case %>Cable !== 'object' || <%= action_name_camel_case %>Cable === null) {
|
|
8
|
+
<%= action_name_camel_case %>Cable = App.cable.subscriptions.create("ActivityLogChannel", {
|
|
9
|
+
connected() {
|
|
10
|
+
console.log("Connected to the channel:", this);
|
|
11
|
+
this.send({ message: '<%= file_name %> Client is connected', topic: "<%= file_name %>", namespace: "subscriptions" });
|
|
12
|
+
},
|
|
13
|
+
disconnected() {
|
|
14
|
+
console.log("<%= file_name %> Client Disconnected");
|
|
15
|
+
},
|
|
16
|
+
received(data) {
|
|
17
|
+
if(data["topic"] == "<%= file_name %>") {
|
|
18
|
+
console.log("<%= file_name %>", data);
|
|
19
|
+
document.getElementById('<%= file_name %>-response').innerHTML = data["message"];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
// Send a message to the server
|
|
25
|
+
<%= action_name_camel_case %>Cable.send({ message: '<%= file_name %> Client is sending a message', topic: "<%= file_name %>", namespace: "subscriptions" });
|
|
26
|
+
// Attach a click event listener to the button which sends an XHR GET request and shows the response.
|
|
27
|
+
// The URL is read from the data-url attribute to avoid ERB interpolation in plain .js files.
|
|
28
|
+
document.getElementById('<%= file_name %>-id').addEventListener('click', function() {
|
|
29
|
+
var url = this.dataset.url;
|
|
30
|
+
var xhr = new XMLHttpRequest();
|
|
31
|
+
xhr.open('GET', url, true);
|
|
32
|
+
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
|
|
33
|
+
xhr.onreadystatechange = function() {
|
|
34
|
+
if (xhr.readyState == 4 && xhr.status == 200) {
|
|
35
|
+
var response = JSON.parse(xhr.responseText);
|
|
36
|
+
document.getElementById('<%= file_name %>-response').innerHTML = response.message;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
xhr.send();
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Attach the function to the eventListener
|
|
44
|
+
document.addEventListener('turbo:load', <%= action_name_camel_case %>Function);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
RailsAdmin::Config::Actions.add_action "<%= file_name %>", :base, :member do
|
|
2
|
+
link_icon 'fas fa-file'
|
|
3
|
+
http_methods [:get, :patch]
|
|
4
|
+
# Customize visibility: show this action only for specific models.
|
|
5
|
+
# Example: visible only for the User model:
|
|
6
|
+
# visible do
|
|
7
|
+
# bindings[:object].is_a?(::User)
|
|
8
|
+
# end
|
|
9
|
+
# Or use the default authorization check:
|
|
10
|
+
visible? authorized?
|
|
11
|
+
# Adding the controller which is needed to compute calls from the ui
|
|
12
|
+
controller do
|
|
13
|
+
proc do
|
|
14
|
+
# if it's a form submission, then update the password
|
|
15
|
+
if !request.xhr? && request.patch?
|
|
16
|
+
flash[:success] = I18n.t("Successfully clicked on sample action")
|
|
17
|
+
# Redirect to the object
|
|
18
|
+
redirect_to index_path(model_name: @abstract_model.to_param)
|
|
19
|
+
elsif request.xhr? && request.get?
|
|
20
|
+
# Return a json response
|
|
21
|
+
render json: { message: "Hello from <%= file_name %>" }, status: :ok
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Spinner
|
|
2
|
+
.loader {
|
|
3
|
+
width: 40px;
|
|
4
|
+
height: 40px;
|
|
5
|
+
position: relative;
|
|
6
|
+
margin: 0 auto;
|
|
7
|
+
}
|
|
8
|
+
.double-bounce1,
|
|
9
|
+
.double-bounce2 {
|
|
10
|
+
width: 100%;
|
|
11
|
+
height: 100%;
|
|
12
|
+
border-radius: 50%;
|
|
13
|
+
background-color: #333;
|
|
14
|
+
opacity: 0.6;
|
|
15
|
+
position: absolute;
|
|
16
|
+
top: 0;
|
|
17
|
+
left: 0;
|
|
18
|
+
animation: sk-bounce 2.0s infinite ease-in-out;
|
|
19
|
+
}
|
|
20
|
+
.double-bounce2 {
|
|
21
|
+
animation-delay: -1.0s;
|
|
22
|
+
}
|
|
23
|
+
@keyframes sk-bounce {
|
|
24
|
+
0%,
|
|
25
|
+
100% {
|
|
26
|
+
transform: scale(0.0);
|
|
27
|
+
}
|
|
28
|
+
50% {
|
|
29
|
+
transform: scale(1.0);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// End Spinner
|
|
33
|
+
#<%= file_name %>-response {
|
|
34
|
+
border-radius: 1em;
|
|
35
|
+
display: flex;
|
|
36
|
+
flex-direction: column;
|
|
37
|
+
justify-content: center;
|
|
38
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require "rails/generators/active_record/migration/migration_generator"
|
|
2
2
|
require "generators/thecore/atom_aware"
|
|
3
|
+
require "generators/thecore/association_wiring"
|
|
3
4
|
|
|
4
5
|
module Thecore
|
|
5
6
|
module Generators
|
|
@@ -9,17 +10,27 @@ module Thecore
|
|
|
9
10
|
# Thecore::Generators::ModelGenerator for the namespace-resolution
|
|
10
11
|
# mechanism, identical here).
|
|
11
12
|
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
# Thecore::Generators::AtomAware
|
|
16
|
-
#
|
|
17
|
-
# `rails generate migration AddBarToFoo bar:string` gets the same
|
|
18
|
-
#
|
|
13
|
+
# 100% of ActiveRecord::Generators::MigrationGenerator's migration-content
|
|
14
|
+
# logic (add/remove/create-table detection, attribute parsing, templates)
|
|
15
|
+
# is inherited untouched. On top of that:
|
|
16
|
+
# - Thecore::Generators::AtomAware redirects the migration file into an
|
|
17
|
+
# ATOM's db/migrate when one is detected from `Dir.pwd`/`--atom=NAME`,
|
|
18
|
+
# so `rails generate migration AddBarToFoo bar:string` gets the same
|
|
19
|
+
# context-aware placement standalone, not just via `rails generate
|
|
20
|
+
# model`.
|
|
21
|
+
# - Thecore::Generators::AssociationWiring detects `references`
|
|
22
|
+
# attributes and writes the missing inverse `has_many`/`has_one` side
|
|
23
|
+
# into the target model's concern (ADR 0003 in the thecore repo).
|
|
19
24
|
class MigrationGenerator < ActiveRecord::Generators::MigrationGenerator
|
|
20
25
|
include Thecore::Generators::AtomAware
|
|
26
|
+
include Thecore::Generators::AssociationWiring
|
|
21
27
|
|
|
22
28
|
source_root ActiveRecord::Generators::MigrationGenerator.source_root
|
|
29
|
+
|
|
30
|
+
def create_migration_file
|
|
31
|
+
super
|
|
32
|
+
wire_inverse_associations_from_references
|
|
33
|
+
end
|
|
23
34
|
end
|
|
24
35
|
end
|
|
25
36
|
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require "rails/generators/active_record/model/model_generator"
|
|
2
2
|
require "generators/thecore/atom_aware"
|
|
3
|
+
require "generators/thecore/association_wiring"
|
|
3
4
|
|
|
4
5
|
module Thecore
|
|
5
6
|
module Generators
|
|
@@ -34,8 +35,16 @@ module Thecore
|
|
|
34
35
|
# - Test file generation is never suppressed (no `--skip-test-framework`
|
|
35
36
|
# equivalent) — `hook_for :test_framework` runs exactly as it does for
|
|
36
37
|
# `active_record:model`.
|
|
38
|
+
# - Thecore::Generators::AssociationWiring detects `references`
|
|
39
|
+
# attributes and writes the missing inverse `has_many`/`has_one` side
|
|
40
|
+
# into the target model's concern (ADR 0003 in the thecore repo) —
|
|
41
|
+
# needed here too, not just in MigrationGenerator, because
|
|
42
|
+
# `rails generate model Foo x:references` creates its migration via
|
|
43
|
+
# ActiveRecord::Generators::ModelGenerator#create_migration_file, a
|
|
44
|
+
# different method than MigrationGenerator's own.
|
|
37
45
|
class ModelGenerator < ActiveRecord::Generators::ModelGenerator
|
|
38
46
|
include Thecore::Generators::AtomAware
|
|
47
|
+
include Thecore::Generators::AssociationWiring
|
|
39
48
|
|
|
40
49
|
class_option :with_api_concern, type: :boolean, default: false,
|
|
41
50
|
desc: "Scaffold a starter app/models/concerns/api/<model>.rb, included into the model " \
|
|
@@ -62,6 +71,11 @@ module Thecore
|
|
|
62
71
|
add_opted_in_concerns
|
|
63
72
|
end
|
|
64
73
|
|
|
74
|
+
def create_migration_file
|
|
75
|
+
super
|
|
76
|
+
wire_inverse_associations_from_references
|
|
77
|
+
end
|
|
78
|
+
|
|
65
79
|
private
|
|
66
80
|
|
|
67
81
|
# Faithful Ruby port of addModel.js's api_concern.rb/rails_admin_concern.rb
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
require "rails/generators/named_base"
|
|
2
|
+
require "generators/thecore/atom_aware"
|
|
3
|
+
require "generators/thecore/companion_files"
|
|
4
|
+
require "generators/thecore/action_companion"
|
|
5
|
+
|
|
6
|
+
module Thecore
|
|
7
|
+
module Generators
|
|
8
|
+
# `rails generate thecore:root_action NAME` — a Ruby port of
|
|
9
|
+
# thecore_code_extension's addRootAction.js (thecore_generators#11),
|
|
10
|
+
# producing the same end result from a terminal: the RailsAdmin root
|
|
11
|
+
# action file, its view/JS/SCSS companions, the after_initialize.rb
|
|
12
|
+
# require line, the assets.rb precompile line, and locale entries — with
|
|
13
|
+
# ATOM-aware placement via the same Thecore::Generators::AtomAware
|
|
14
|
+
# mechanism the Model/Migration generators already use.
|
|
15
|
+
#
|
|
16
|
+
# Discovered automatically by Rails::Generators' own namespace-by-path
|
|
17
|
+
# convention (this file's path derives the "thecore:root_action"
|
|
18
|
+
# namespace) — no Railtie registration needed, unlike
|
|
19
|
+
# ModelGenerator/MigrationGenerator, since there is no built-in Rails
|
|
20
|
+
# generator being overridden here.
|
|
21
|
+
#
|
|
22
|
+
# Unlike ModelGenerator/MigrationGenerator, Thecore::Generators::AtomAware
|
|
23
|
+
# only supplies `atom_dir`/`host_app_root` and the `--atom` option here —
|
|
24
|
+
# this generator does not override `destination_root` placement for a
|
|
25
|
+
# single fixed subpath the way Model/Migration's templates do, because the
|
|
26
|
+
# action file itself lives at a *different* relative path depending on
|
|
27
|
+
# context (ATOM: lib/root_actions/, host app: config/root_actions/ — see
|
|
28
|
+
# docs/adr/0001-main-app-actions-live-in-config.md in
|
|
29
|
+
# thecore_code_extension for why the host-app side avoids lib/). AtomAware
|
|
30
|
+
# still redirects `destination_root` into the ATOM dir when one is
|
|
31
|
+
# detected, so the view/JS/SCSS/locale/after_initialize/assets companions
|
|
32
|
+
# (fixed relative paths, shared with the host-app case) land in the right
|
|
33
|
+
# place automatically.
|
|
34
|
+
#
|
|
35
|
+
# The full generator step sequence (validate → create_action_file →
|
|
36
|
+
# create_view_js_scss_companions → add_after_initialize_require →
|
|
37
|
+
# add_assets_precompile_line → add_locale_entries) and everything about
|
|
38
|
+
# placement/naming lives in Thecore::Generators::ActionCompanion, shared
|
|
39
|
+
# with MemberActionGenerator (thecore_generators#12) — only this class's
|
|
40
|
+
# own `templates/action.rb.tt`/`action.html.erb.tt`/`action.js.tt` (the
|
|
41
|
+
# RailsAdmin `:root` action type and its fetch + ActionCable-broadcast
|
|
42
|
+
# example) are Root-specific.
|
|
43
|
+
class RootActionGenerator < Rails::Generators::NamedBase
|
|
44
|
+
include Thecore::Generators::AtomAware
|
|
45
|
+
include Thecore::Generators::CompanionFiles
|
|
46
|
+
include Thecore::Generators::ActionCompanion
|
|
47
|
+
|
|
48
|
+
action_kind "root_action"
|
|
49
|
+
|
|
50
|
+
source_root File.expand_path("templates", __dir__)
|
|
51
|
+
|
|
52
|
+
# Thin task methods, required on each class directly (see
|
|
53
|
+
# ActionCompanion's own comment for why) - each delegates to shared
|
|
54
|
+
# private logic there.
|
|
55
|
+
def validate_action_name!
|
|
56
|
+
validate_action_name_for_kind!
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def create_action_file
|
|
60
|
+
template "action.rb.tt", action_file_path
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def create_view_js_scss_companions
|
|
64
|
+
render_view_js_scss_companions!
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def add_after_initialize_require
|
|
68
|
+
ensure_after_initialize_require!(require_line)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def add_assets_precompile_line
|
|
72
|
+
ensure_assets_precompile_line!(assets_precompile_line)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def add_locale_entries
|
|
76
|
+
write_action_locale_entries!(file_name, title_case_name)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<%%= stylesheet_link_tag 'rails_admin/actions/<%= file_name %>' %>
|
|
2
|
+
<div class="card mb-3">
|
|
3
|
+
<div class="card-body">
|
|
4
|
+
<div class="response <%= file_name %>-response" id="<%= file_name %>-response">
|
|
5
|
+
</div>
|
|
6
|
+
<div class="loader d-none" id="<%= file_name %>-loader">
|
|
7
|
+
<div class="double-bounce1"></div>
|
|
8
|
+
<div class="double-bounce2"></div>
|
|
9
|
+
</div>
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
<button class="btn btn-primary" id="<%= file_name %>-id" data-url="<%%= rails_admin.<%= file_name %>_path %>">Click me</button>
|
|
13
|
+
<%%= javascript_include_tag "rails_admin/actions/<%= file_name %>" %>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
var <%= action_name_camel_case %>Cable = null;
|
|
2
|
+
// If the <%= action_name_camel_case %>Function is already defined, then don't redefine it and don't attach it to the eventListener
|
|
3
|
+
if (typeof <%= action_name_camel_case %>Function !== 'function') {
|
|
4
|
+
function <%= action_name_camel_case %>Function(event) {
|
|
5
|
+
console.log('Hello from <%= file_name %>', event);
|
|
6
|
+
// Action Cable WebSocket connection only if <%= action_name_camel_case %>Cable is not already defined and valid
|
|
7
|
+
if (typeof <%= action_name_camel_case %>Cable !== 'object' || <%= action_name_camel_case %>Cable === null) {
|
|
8
|
+
<%= action_name_camel_case %>Cable = App.cable.subscriptions.create("ActivityLogChannel", {
|
|
9
|
+
connected() {
|
|
10
|
+
console.log("Connected to the channel:", this);
|
|
11
|
+
this.send({ message: '<%= file_name %> Client is connected', topic: "<%= file_name %>", namespace: "subscriptions" });
|
|
12
|
+
},
|
|
13
|
+
disconnected() {
|
|
14
|
+
console.log("<%= file_name %> Client Disconnected");
|
|
15
|
+
},
|
|
16
|
+
received(data) {
|
|
17
|
+
if(data["topic"] == "<%= file_name %>") {
|
|
18
|
+
console.log("<%= file_name %>", data);
|
|
19
|
+
document.getElementById('<%= file_name %>-response').innerHTML = data["message"];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
// Send a message to the server
|
|
25
|
+
<%= action_name_camel_case %>Cable.send({ message: '<%= file_name %> Client is sending a message', topic: "<%= file_name %>", namespace: "subscriptions" });
|
|
26
|
+
// Attach a click event listener to the button which sends a fetch GET request and shows the response.
|
|
27
|
+
// The URL is read from the data-url attribute to avoid ERB interpolation in plain .js files.
|
|
28
|
+
document.getElementById('<%= file_name %>-id').addEventListener('click', function() {
|
|
29
|
+
var url = this.dataset.url;
|
|
30
|
+
document.getElementById('<%= file_name %>-loader').classList.remove('d-none');
|
|
31
|
+
fetch(url, { headers: { 'Accept': 'application/json' } })
|
|
32
|
+
.then(response => response.json())
|
|
33
|
+
.then(data => {
|
|
34
|
+
console.log(data);
|
|
35
|
+
document.getElementById('<%= file_name %>-response').innerHTML = data.message;
|
|
36
|
+
document.getElementById('<%= file_name %>-loader').classList.add('d-none');
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// Attach the function to the eventListener
|
|
42
|
+
document.addEventListener('turbo:load', <%= action_name_camel_case %>Function);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
RailsAdmin::Config::Actions.add_action "<%= file_name %>", :base, :root do
|
|
2
|
+
show_in_sidebar true
|
|
3
|
+
show_in_navigation false
|
|
4
|
+
breadcrumb_parent [nil]
|
|
5
|
+
# This ensures the action only shows up for authorized users
|
|
6
|
+
visible? authorized?
|
|
7
|
+
# Not a member action
|
|
8
|
+
member false
|
|
9
|
+
# Not a collection action
|
|
10
|
+
collection false
|
|
11
|
+
# Have a look at https://fontawesome.com/v5/search for available icons
|
|
12
|
+
link_icon 'fas fa-file'
|
|
13
|
+
# The controller which will be used to compute the action and the REST verbs it will respond to
|
|
14
|
+
http_methods [:get]
|
|
15
|
+
# Adding the controller which is needed to compute calls from the ui
|
|
16
|
+
controller do
|
|
17
|
+
proc do # This is needed because we need that this code is re-evaluated each time is called
|
|
18
|
+
if request.format.json?
|
|
19
|
+
# This is the code that is executed when the action is called
|
|
20
|
+
# It is executed in the context of the controller
|
|
21
|
+
# So you can access all the controller methods
|
|
22
|
+
# and instance variables
|
|
23
|
+
status = 200
|
|
24
|
+
message = "Hello World!"
|
|
25
|
+
# Note: ActivityLogChannel is expected to re-broadcast messages from the "messages" channel
|
|
26
|
+
ActionCable.server.broadcast("messages", { topic: :<%= file_name %>, status: status, message: message})
|
|
27
|
+
render json: {message: message}.to_json, status: status
|
|
28
|
+
else
|
|
29
|
+
# Renders the action.html.erb view for browser requests (HTML format)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Spinner
|
|
2
|
+
.loader {
|
|
3
|
+
width: 40px;
|
|
4
|
+
height: 40px;
|
|
5
|
+
position: relative;
|
|
6
|
+
margin: 0 auto;
|
|
7
|
+
}
|
|
8
|
+
.double-bounce1,
|
|
9
|
+
.double-bounce2 {
|
|
10
|
+
width: 100%;
|
|
11
|
+
height: 100%;
|
|
12
|
+
border-radius: 50%;
|
|
13
|
+
background-color: #333;
|
|
14
|
+
opacity: 0.6;
|
|
15
|
+
position: absolute;
|
|
16
|
+
top: 0;
|
|
17
|
+
left: 0;
|
|
18
|
+
animation: sk-bounce 2.0s infinite ease-in-out;
|
|
19
|
+
}
|
|
20
|
+
.double-bounce2 {
|
|
21
|
+
animation-delay: -1.0s;
|
|
22
|
+
}
|
|
23
|
+
@keyframes sk-bounce {
|
|
24
|
+
0%,
|
|
25
|
+
100% {
|
|
26
|
+
transform: scale(0.0);
|
|
27
|
+
}
|
|
28
|
+
50% {
|
|
29
|
+
transform: scale(1.0);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// End Spinner
|
|
33
|
+
#<%= file_name %>-response {
|
|
34
|
+
border-radius: 1em;
|
|
35
|
+
display: flex;
|
|
36
|
+
flex-direction: column;
|
|
37
|
+
justify-content: center;
|
|
38
|
+
}
|