five-two-nw-olivander 0.2.0.49 → 0.2.0.51

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.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/controllers/datatable_index_charts_controller.js +5 -2
  3. data/app/components/olivander/components/menu_item_component.rb +0 -1
  4. data/app/components/olivander/components/resource_form_component.html.haml +1 -1
  5. data/app/components/olivander/components/resource_form_component.rb +0 -1
  6. data/app/components/olivander/components/resource_show_component.html.haml +1 -1
  7. data/app/components/olivander/components/resource_show_component.rb +0 -1
  8. data/app/components/olivander/components/tabs_component.rb +1 -1
  9. data/app/controllers/concerns/olivander/resources/auto_form_attributes.rb +109 -38
  10. data/app/controllers/concerns/olivander/resources/route_builder.rb +49 -35
  11. data/app/views/application/index.html.haml +4 -1
  12. data/lib/generators/olivander/active_record/model/templates/model.rb.tt +32 -0
  13. data/lib/generators/olivander/active_record/model/templates/module.rb.tt +7 -0
  14. data/lib/generators/olivander/active_record_generator.rb +21 -0
  15. data/lib/generators/olivander/controller/templates/controller.rb.tt +8 -0
  16. data/lib/generators/olivander/controller_generator.rb +7 -0
  17. data/lib/generators/olivander/datatable_generator.rb +13 -0
  18. data/lib/generators/olivander/infrastructure_generator.rb +76 -0
  19. data/lib/generators/olivander/migration/templates/create_table_migration.rb.tt +27 -0
  20. data/lib/generators/olivander/model_generator.rb +7 -0
  21. data/lib/generators/olivander/scaffold_generator.rb +117 -0
  22. data/lib/generators/olivander/templates/datatable/datatable.rb.tt +29 -0
  23. data/lib/generators/olivander/templates/infrastructure/context_builder.rb.tt +16 -0
  24. data/lib/generators/olivander/templates/infrastructure/hello_olivander_controller.rb.tt +11 -0
  25. data/lib/generators/olivander/templates/infrastructure/manifest.js +5 -0
  26. data/lib/generators/olivander/templates/infrastructure/menu_builder.rb.tt +32 -0
  27. data/lib/generators/olivander/templates/infrastructure/route_builder.rb.tt +8 -0
  28. data/lib/olivander/version.rb +1 -1
  29. metadata +18 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7d230d3b035fa0ef2848260e9a18935d703332e40ae67e1f3ac82720859ed681
4
- data.tar.gz: 25d75dde2112dee7691f9e01f02da3383b7f86fb1a5bebf21516fffbef504443
3
+ metadata.gz: e178485d7c7d6e52a13df286378873fadb049edcbc84a88de71763db0f29dc02
4
+ data.tar.gz: 8e60799ed84eb44f378741a5df3d95d48159837ce752fda30a5b5deac7604cc7
5
5
  SHA512:
6
- metadata.gz: 64885a09cd9644a5eb035605186ec6ccfd711906772a81ff62b3ecf8523c78cf7ed2fd6a6eeb4e349e19779591f09b7f02516dd6ab06f8a4a0520aca8af0a212
7
- data.tar.gz: 95a26ab22fb487d7fae8646898457fc77fbd77d02dd0a5fa8439931a892b1da3e05c80ac23307c9d2db6a0f3561f0c727abcf4975f93315679b913761b201a89
6
+ metadata.gz: 1597949c45595dca40b42e52a6434f7c4317b7d8cead7694b0167552140d591207700341562951ad0c526099ba1d6a466cfb111205c4adcc595289a48b83ffa3
7
+ data.tar.gz: c256983a41fc1c851e311fd6675ddd4c89db14f4ea36b4591ef1670f6f91c838d001f8372cd7e694380771ac0dd206f3b757c88f229e5ce44d2a4b0a726232db
@@ -20,8 +20,11 @@ export default class extends Controller {
20
20
  transformData(chart) {
21
21
  var self = this
22
22
  if (chart.as == 'LineChart') {
23
- // we don't know what to do
24
- return chart.data
23
+ var transformed = chart.data.map(([name, obj]) => ({
24
+ name,
25
+ data: Object.entries(obj || {})
26
+ }));
27
+ return transformed
25
28
  } else {
26
29
  return chart.data
27
30
  }
@@ -4,7 +4,6 @@ class Olivander::Components::MenuItemComponent < ViewComponent::Base
4
4
  attr_reader :menu_item
5
5
 
6
6
  def initialize(menu_item)
7
- super
8
7
  @menu_item = menu_item
9
8
  end
10
9
  end
@@ -3,7 +3,7 @@
3
3
  %h3= resource_field_group_label(@resource.class, rfg.key) unless rfg.key == :default
4
4
  - rfg.sections.each do |section|
5
5
  .row
6
- - section.fields.each do |field|
6
+ - section.form_fields.each do |field|
7
7
  - label = field_label_for(@resource.class, field.sym)
8
8
  %div{ class: section.column_class }
9
9
  - if association?(field)
@@ -7,7 +7,6 @@ class Olivander::Components::ResourceFormComponent < ViewComponent::Base
7
7
  def initialize(resource, form_builder)
8
8
  @resource = resource
9
9
  @f = form_builder
10
- super
11
10
  end
12
11
 
13
12
  def collection_for(field)
@@ -7,7 +7,7 @@
7
7
  %td{ style: 'width: 8.33%; height: 0px' }
8
8
  %tbody
9
9
  - rfg.sections.each do |section|
10
- - section.fields.each_slice(section.columns) do |slice|
10
+ - section.show_fields.each_slice(section.columns) do |slice|
11
11
  - colspan = (12 - slice.size) / section.columns
12
12
  %tr
13
13
  - slice.each do |f|
@@ -7,6 +7,5 @@ class Olivander::Components::ResourceShowComponent < ViewComponent::Base
7
7
  def initialize(resource, actions)
8
8
  @resource = resource
9
9
  @actions = actions
10
- super
11
10
  end
12
11
  end
@@ -14,7 +14,7 @@ module Olivander
14
14
  options = args.extract_options!
15
15
  @id = options[:id] || "tabs-#{SecureRandom.hex(4)}"
16
16
  @card = options.key?(:card) ? !!options[:card] : true
17
- @card_class = options.key?(:card_class) ? !!options[:card_class] : 'card-primary'
17
+ @card_class = options.key?(:card_class) ? options[:card_class] : 'card-primary'
18
18
  @tab_strip_id = "tab-strip-#{SecureRandom.hex(4)}"
19
19
  @tab_content_id = "tab-strip-#{SecureRandom.hex(4)}"
20
20
  end
@@ -5,7 +5,7 @@ module Olivander
5
5
 
6
6
  included do
7
7
  def auto_form_attributes
8
- attributes.keys - ['updated_at', 'created_at', 'deleted_at']
8
+ attributes.keys - %w[updated_at created_at deleted_at]
9
9
  end
10
10
 
11
11
  def self.method_missing(m, *args, **kwargs, &block)
@@ -16,6 +16,57 @@ module Olivander
16
16
  super
17
17
  end
18
18
  end
19
+
20
+ def self.tracked_attrs
21
+ @tracked_attrs ||= { readers: [], writers: [], accessors: [] }
22
+ end
23
+
24
+ # when subclassing, make sure the subclass gets a copy of parent's tracked attrs
25
+ def self.inherited(subclass)
26
+ super if defined?(super)
27
+
28
+ # copy parent's lists into subclass so they are independent arrays
29
+ tracked_attrs.each do |key, arr|
30
+ subclass.tracked_attrs[key].concat(arr.dup)
31
+ end
32
+ end
33
+
34
+ # override attr_* to capture names, then delegate to the original behavior
35
+ def self.attr_reader(*names)
36
+ tracked_attrs[:readers].concat(names.map(&:to_sym))
37
+ super
38
+ end
39
+
40
+ def self.attr_writer(*names)
41
+ tracked_attrs[:writers].concat(names.map(&:to_sym))
42
+ super
43
+ end
44
+
45
+ def self.attr_accessor(*names)
46
+ tracked_attrs[:accessors].concat(names.map(&:to_sym))
47
+ super
48
+ end
49
+
50
+ # accessors for the tracked data (unique and preserved order)
51
+ def self.tracked_attrs
52
+ @tracked_attrs ||= { readers: [], writers: [], accessors: [] }
53
+ end
54
+
55
+ def self.readers
56
+ tracked_attrs[:readers].uniq
57
+ end
58
+
59
+ def self.writers
60
+ tracked_attrs[:writers].uniq
61
+ end
62
+
63
+ def self.accessors
64
+ tracked_attrs[:accessors].uniq
65
+ end
66
+
67
+ def self.all_tracked_attributes
68
+ (readers + writers + accessors).uniq
69
+ end
19
70
  end
20
71
  end
21
72
 
@@ -29,54 +80,60 @@ module Olivander
29
80
  cattr_accessor :resource_field_group_collection
30
81
 
31
82
  def self.resource_field_groups
32
- auto_resource_fields if self.resource_field_group_collection.nil?
33
- self.resource_field_group_collection
83
+ auto_resource_fields if resource_field_group_collection.nil?
84
+ resource_field_group_collection
34
85
  end
35
86
 
36
- def self.auto_resource_fields(columns: 2, only: [], except: [], editable: true)
87
+ def self.auto_resource_fields(columns: 2, only: [], except: [], editable: true, on_show: true, on_form: true)
37
88
  return unless ActiveRecord::Base.connection.table_exists?(table_name)
38
89
 
39
90
  if current_resource_field_group.nil?
40
- resource_field_group do
41
- auto_resource_fields(columns: columns, only: only, except: except, editable: editable)
91
+ resource_field_group(editable: editable, on_show: on_show, on_form: on_form) do
92
+ auto_resource_fields(columns: columns, only: only, except: except, editable: editable, on_show: on_show,
93
+ on_form: on_form)
42
94
  end
43
95
  elsif current_resource_field_group.forced_section.nil?
44
96
  resource_field_section(columns) do
45
- auto_resource_fields(columns: columns, only: only, except: except, editable: editable)
97
+ auto_resource_fields(columns: columns, only: only, except: except, editable: editable, on_show: on_show,
98
+ on_form: on_form)
46
99
  end
47
100
  else
48
101
  if only.size.zero?
49
102
  only = [
50
- self.columns.collect{ |x| x.name.to_sym },
51
- reflections.map{ |r| r[1].name },
103
+ self.columns.collect { |x| x.name.to_sym },
104
+ reflections.map { |r| r[1].name }
52
105
  ]
53
- only << attachment_definitions.select{ |x| x[0] } if respond_to?(:attachment_definitions)
106
+ only << attachment_definitions.select { |x| x[0] } if respond_to?(:attachment_definitions)
54
107
  only = only.flatten - SKIPPED_ATTRIBUTES
55
108
  end
56
- only = only - except
109
+ only -= except
57
110
  only.each do |inc|
111
+ all_tracked_attributes.each do |sym|
112
+ next unless inc == sym
113
+
114
+ resource_field sym, :string, editable: false, on_show: true, on_form: false
115
+ end
116
+
58
117
  self.columns.each do |att|
59
118
  sym = att.name.to_sym
60
119
  type = att.type
61
120
  next unless inc == sym
62
121
 
63
- resource_field sym, type, editable: editable
122
+ resource_field sym, type, editable: editable, on_show: on_show, on_form: on_form
64
123
  end
65
124
 
66
- reflections.map{ |x| x[1] }
67
- .filter{ |x| x.foreign_key == inc || x.name == inc }
125
+ reflections.map { |x| x[1] }
126
+ .filter { |x| x.foreign_key == inc || x.name == inc }
68
127
  .each do |r|
69
- begin
70
- type = r.association_class.name.demodulize.underscore.to_sym
71
- resource_field(r.name, type, editable: editable && !uneditable_association?(r, type))
72
- rescue NotImplementedError
73
- resource_field(r.name, :association, editable: editable && !uneditable_association?(r, type))
74
- end
128
+ type = r.association_class.name.demodulize.underscore.to_sym
129
+ resource_field(r.name, type, editable: editable && !uneditable_association?(r, type))
130
+ rescue NotImplementedError
131
+ resource_field(r.name, :association, editable: editable && !uneditable_association?(r, type))
75
132
  end
76
133
 
77
134
  next unless respond_to?(:attachment_definitions)
78
135
 
79
- attachment_definitions.filter{ |x| x == inc }.each do |ad|
136
+ attachment_definitions.filter { |x| x == inc }.each do |ad|
80
137
  resource_field ad[0], :file, editable: editable
81
138
  end
82
139
  end
@@ -90,42 +147,46 @@ module Olivander
90
147
  %i[has_one_through_association].include?(type)
91
148
  end
92
149
 
93
- def self.resource_field_group(key = :default, editable: true, &block)
150
+ def self.resource_field_group(key = :default, editable: true, on_show: true, on_form: true)
94
151
  self.resource_field_group_collection ||= []
95
- self.current_resource_field_group = resource_field_group_collection.select{ |x| x.key == key}.first
152
+ self.current_resource_field_group = resource_field_group_collection.select { |x| x.key == key }.first
96
153
  unless current_resource_field_group.present?
97
- self.current_resource_field_group = ResourceFieldGroup.new(key, editable)
98
- self.resource_field_group_collection << self.current_resource_field_group
154
+ self.current_resource_field_group = ResourceFieldGroup.new(key, editable, on_show, on_form)
155
+ self.resource_field_group_collection << current_resource_field_group
99
156
  end
100
157
  yield
101
158
  self.current_resource_field_group = nil
102
159
  end
103
160
 
104
- def self.resource_field_section(columns = nil, &block)
105
- self.current_resource_field_group.forced_section = self.current_resource_field_group.next_section(columns)
161
+ def self.resource_field_section(columns = nil)
162
+ current_resource_field_group.forced_section = current_resource_field_group.next_section(columns)
106
163
  yield
107
- self.current_resource_field_group.forced_section = nil
164
+ current_resource_field_group.forced_section = nil
108
165
  end
109
166
 
110
- def self.resource_field(sym, type = :string, editable: nil)
111
- self.current_resource_field_group.add_field(sym, type, editable)
167
+ def self.resource_field(sym, type = :string, editable: nil, on_show: true, on_form: true)
168
+ current_resource_field_group.add_field(sym, type, editable, on_show, on_form)
112
169
  end
113
170
  end
114
171
 
115
172
  class ResourceFieldGroup
116
- attr_accessor :fields, :key, :editable, :forced_section, :sections
173
+ attr_accessor :fields, :key, :editable, :forced_section, :sections, :on_show, :on_form
117
174
 
118
- def initialize(key, editable)
175
+ def initialize(key, editable, on_show, on_form)
119
176
  self.key = key
120
177
  self.editable = editable
178
+ self.on_show = on_show
179
+ self.on_form = on_form
121
180
  self.fields = []
122
181
  self.sections = []
123
182
  end
124
183
 
125
- def add_field(sym, type, editable)
184
+ def add_field(sym, type, editable, on_show, on_form)
126
185
  e = editable.nil? ? self.editable : editable
186
+ s = on_show
187
+ f = on_form
127
188
  section = forced_section || next_section
128
- field = ResourceField.new(sym, type, e, self)
189
+ field = ResourceField.new(sym, type, e, s, f, self)
129
190
  section.fields << field
130
191
  fields << field
131
192
  end
@@ -137,7 +198,7 @@ module Olivander
137
198
  end
138
199
 
139
200
  def max_section_columns
140
- sections.collect{ |x| x.columns }.max
201
+ sections.collect { |x| x.columns }.max
141
202
  end
142
203
  end
143
204
 
@@ -150,16 +211,26 @@ module Olivander
150
211
  end
151
212
 
152
213
  def column_class
153
- "col-md-#{12/columns}"
214
+ "col-md-#{12 / columns}"
215
+ end
216
+
217
+ def show_fields
218
+ fields.select { |f| f.on_show }
219
+ end
220
+
221
+ def form_fields
222
+ fields.select { |f| f.on_form }
154
223
  end
155
224
  end
156
225
 
157
226
  class ResourceField
158
- attr_accessor :sym, :type, :editable
227
+ attr_accessor :sym, :type, :editable, :on_show, :on_form
159
228
 
160
- def initialize(sym, type, editable, group)
229
+ def initialize(sym, type, editable, on_show, on_form, _group)
161
230
  self.sym = sym
162
231
  self.type = type
232
+ self.on_show = on_show
233
+ self.on_form = on_form
163
234
  self.editable = editable
164
235
  end
165
236
  end
@@ -63,10 +63,11 @@ module Olivander
63
63
  end
64
64
 
65
65
  class RoutedResource
66
- attr_accessor :model, :namespaces, :actions
66
+ attr_accessor :model, :nested_models, :namespaces, :actions
67
67
 
68
68
  def initialize(model, namespaces, crud_actions)
69
69
  self.model = model
70
+ self.nested_models = []
70
71
  self.namespaces = namespaces
71
72
  self.actions = []
72
73
  %i[index new create edit show update destroy].each do |ca|
@@ -134,10 +135,15 @@ module Olivander
134
135
 
135
136
  class_methods do
136
137
  def resource(model, only: DEFAULT_CRUD_ACTIONS, except: [], namespaces: [])
138
+ parent_resource = self.current_resource
137
139
  self.current_resource = RoutedResource.new(model, namespaces, DEFAULT_CRUD_ACTIONS & (only - except))
140
+ if parent_resource
141
+ parent_resource.nested_models << self.current_resource
142
+ else
143
+ resources[model] = current_resource
144
+ end
138
145
  yield if block_given?
139
- resources[model] = current_resource
140
- self.current_resource = nil
146
+ self.current_resource = parent_resource
141
147
  end
142
148
 
143
149
  def action(sym, **kwargs)
@@ -171,44 +177,52 @@ module Olivander
171
177
  build_resource_route(mapper, r, r.namespaces.last(r.namespaces.size - 1))
172
178
  end
173
179
  else
174
- mapper.resources r.model, only: [] do
175
- mapper.collection do
176
- r.collection_actions.each do |ba|
177
- next if ba.no_route
178
- next if ba.action == :new
179
-
180
- if ba.confirm
181
- mapper.get ba.action, action: "confirm_#{ba.action}"
182
- set_controller_and_helper(ba)
183
- mapper.post ba.action
184
- else
185
- mapper.send(ba.verb, ba.action)
186
- set_controller_and_helper(ba)
187
- end
180
+ map_it(mapper, r)
181
+ end
182
+ end
183
+
184
+ def map_it(mapper, r)
185
+ mapper.resources r.model, only: [] do
186
+ mapper.collection do
187
+ r.collection_actions.each do |ba|
188
+ next if ba.no_route
189
+ next if ba.action == :new
190
+
191
+ if ba.confirm
192
+ mapper.get ba.action, action: "confirm_#{ba.action}"
193
+ set_controller_and_helper(ba)
194
+ mapper.post ba.action
195
+ else
196
+ mapper.send(ba.verb, ba.action)
197
+ set_controller_and_helper(ba)
188
198
  end
189
199
  end
190
-
191
- if r.collection_actions.select { |x| x.action == :new }.size.positive?
192
- mapper.new do
193
- mapper.get :new
194
- end
200
+ end
201
+
202
+ if r.collection_actions.select { |x| x.action == :new }.size.positive?
203
+ mapper.new do
204
+ mapper.get :new
195
205
  end
196
-
197
- mapper.member do
198
- r.member_actions.each do |ma|
199
- next if ma.no_route
200
-
201
- if ma.confirm
202
- mapper.get ma.action, action: "confirm_#{ma.action}"
203
- set_controller_and_helper(ma)
204
- mapper.post ma.action
205
- else
206
- mapper.send(ma.verb, ma.action)
207
- set_controller_and_helper(ma)
208
- end
206
+ end
207
+
208
+ mapper.member do
209
+ r.member_actions.each do |ma|
210
+ next if ma.no_route
211
+
212
+ if ma.confirm
213
+ mapper.get ma.action, action: "confirm_#{ma.action}"
214
+ set_controller_and_helper(ma)
215
+ mapper.post ma.action
216
+ else
217
+ mapper.send(ma.verb, ma.action)
218
+ set_controller_and_helper(ma)
209
219
  end
210
220
  end
211
221
  end
222
+
223
+ r.nested_models.each do |nm|
224
+ map_it(mapper, nm)
225
+ end
212
226
  end
213
227
  end
214
228
  end
@@ -23,7 +23,10 @@
23
23
  %button.btn.btn-tool{ type: :button, 'data-card-widget': :maximize }
24
24
  %i.fas.fa-expand
25
25
  .card-body{ style: 'height: 220px' }
26
- = send(chart[:as].underscore, @datatable.to_json[:charts][k][:data], id: chart[:name], height: '90%', adapter: 'google')
26
+ - if chart[:as] == 'LineChart'
27
+ = send(chart[:as].underscore, @datatable.to_json[:charts][k][:data].map{ |x| {name: x[0], data: x[1]}}, id: chart[:name], height: '90%')
28
+ - else
29
+ = send(chart[:as].underscore, @datatable.to_json[:charts][k][:data], id: chart[:name], height: '90%', adapter: 'google')
27
30
 
28
31
  = content_for :datatable_charts
29
32
 
@@ -0,0 +1,32 @@
1
+ <% module_namespacing do -%>
2
+ # frozen_string_literal: true
3
+
4
+ # Model class for <%= plural_table_name %> table
5
+ class <%= class_name %> < <%= parent_class_name.classify %>
6
+ include HagridChassis::MultiTenant
7
+ include HagridChassis::Audited
8
+
9
+ <% attributes.select(&:reference?).each do |attribute| -%>
10
+ belongs_to :<%= attribute.name %><%= ", polymorphic: true" if attribute.polymorphic? %>
11
+ <% end -%>
12
+ <% attributes.select(&:rich_text?).each do |attribute| -%>
13
+ has_rich_text :<%= attribute.name %>
14
+ <% end -%>
15
+ <% attributes.select(&:attachment?).each do |attribute| -%>
16
+ has_one_attached :<%= attribute.name %>
17
+ <% end -%>
18
+ <% attributes.select(&:attachments?).each do |attribute| -%>
19
+ has_many_attached :<%= attribute.name %>
20
+ <% end -%>
21
+ <% attributes.select(&:token?).each do |attribute| -%>
22
+ has_secure_token<% if attribute.name != "token" %> :<%= attribute.name %><% end %>
23
+ <% end -%>
24
+ <% if attributes.any?(&:password_digest?) -%>
25
+ has_secure_password
26
+ <% end -%>
27
+
28
+ effective_resource do
29
+ <% attributes.each do |attribute| -%><%= " #{attribute.name} :#{attribute.type}\n" unless attribute.type == :references %><% end -%>
30
+ end
31
+ end
32
+ <% end -%>
@@ -0,0 +1,7 @@
1
+ <% module_namespacing do -%>
2
+ module <%= class_path.map(&:camelize).join("::") %>
3
+ def self.table_name_prefix
4
+ '<%= namespaced? ? namespaced_class_path.join("_") : class_path.join("_") %>_'
5
+ end
6
+ end
7
+ <% end -%>
@@ -0,0 +1,21 @@
1
+ require "rails/generators/active_record/model/model_generator"
2
+
3
+ module Olivander
4
+ class ActiveRecordGenerator < ActiveRecord::Generators::ModelGenerator
5
+ desc "This will generate ActiveRecord files using Olivander templates"
6
+ argument :attributes, type: :array, default: [], banner: "field[:type][:index] field[:type][:index]"
7
+
8
+ # redefine the source path so we can isolate our templates
9
+ # note that ActiveRecords's generator defines a path like:
10
+ # ../../migration/templates/create_table_migration.rb
11
+ # so we have to put two dummy path positions to counter-act
12
+ # the ../.. and of course append .tt for thor to treat the file
13
+ # as a template
14
+ source_root File.expand_path('active_record/model/templates', __dir__)
15
+
16
+ def create_migration_file
17
+ self.attributes = shell.base.attributes
18
+ super
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,8 @@
1
+ <% module_namespacing do -%>
2
+ # frozen_string_literal: true
3
+
4
+ # Controller class for <%= plural_table_name %> table
5
+ class <%= class_name %>Controller < ApplicationController
6
+ include Olivander::Resources::CrudController
7
+ end
8
+ <% end -%>
@@ -0,0 +1,7 @@
1
+ require 'rails/generators/rails/controller/controller_generator'
2
+
3
+ module Olivander
4
+ class ControllerGenerator < Rails::Generators::ControllerGenerator
5
+ source_root File.expand_path('controller/templates', __dir__)
6
+ end
7
+ end
@@ -0,0 +1,13 @@
1
+ module Olivander
2
+ class DatatableGenerator < Rails::Generators::NamedBase
3
+ include Rails::Generators::ResourceHelpers
4
+
5
+ argument :attributes, type: :array, default: [], banner: "field[:type][:index] field[:type][:index]"
6
+
7
+ source_root File.expand_path('templates/datatable', __dir__)
8
+
9
+ def create_datatable
10
+ template "datatable.rb", File.join("app/datatables", controller_file_path, "../#{file_name}_datatable.rb")
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,76 @@
1
+ module Olivander
2
+ class InfrastructureGenerator < Rails::Generators::Base
3
+ source_root File.expand_path('templates/infrastructure', __dir__)
4
+
5
+ # boolean option — use --no-application-controller to disable
6
+ class_option :application_controller,
7
+ type: :boolean,
8
+ default: true,
9
+ desc: "Modify ApplicationController to include infrastructure hooks"
10
+
11
+ def create_infrastructure
12
+ create_builders
13
+ setup_asset_pipeline
14
+ modify_application_controller
15
+ create_hello_olivander
16
+ route 'root "hello_olivander#index"'
17
+ end
18
+
19
+ private
20
+
21
+ def create_builders
22
+ template "route_builder.rb", File.join("app/services", "route_builder.rb")
23
+ template "context_builder.rb", File.join("app/services", "context_builder.rb")
24
+ template "menu_builder.rb", File.join("app/services", "menu_builder.rb")
25
+ end
26
+
27
+ def setup_asset_pipeline
28
+ template "manifest.js", File.join("app/assets/config", "manifest.js")
29
+ pin_all = 'pin_all_from "#{Olivander.root}/../app/assets/javascripts/controllers", under: "controllers"'
30
+ inject_into_file 'config/importmap.rb', pin_all, verbose: true, force: false
31
+ end
32
+
33
+ def create_hello_olivander
34
+ template "hello_olivander_controller.rb", File.join("app/controllers", "hello_olivander_controller.rb")
35
+ end
36
+
37
+ # Insert a safe snippet into ApplicationController
38
+ def modify_application_controller
39
+ unless options[:application_controller]
40
+ say_status :skip, "ApplicationController modification skipped", :yellow
41
+ return
42
+ end
43
+
44
+ target = File.join("app", "controllers", "application_controller.rb")
45
+ snippet = <<~RUBY
46
+
47
+ # --- Olivander infrastructure additions (generated) ---
48
+ # If you want to remove this, delete the lines between the markers.
49
+ before_action :build_context
50
+
51
+ def build_context
52
+ # can?(:build, :context)
53
+ ContextBuilder.build_context
54
+ end
55
+
56
+ def authorize!(*args)
57
+ # implement authorization logic
58
+ end
59
+ # ------------------------------------------------------
60
+ RUBY
61
+
62
+ if File.exist?(target)
63
+ inject_into_class target, "ApplicationController", snippet.indent(2)
64
+ say_status :insert, "added infrastructure hooks to #{target}", :green
65
+ else
66
+ # create a minimal ApplicationController if none exists
67
+ create_file target, <<~RUBY
68
+ class ApplicationController < ActionController::Base
69
+ #{snippet.indent(2)}
70
+ end
71
+ RUBY
72
+ say_status :create, "#{target} (new)", :green
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,27 @@
1
+ class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
+ def change
3
+ create_table :<%= table_name %><%= primary_key_type %> do |t|
4
+ <% attributes.each do |attribute| -%>
5
+ <% foreign_key_type ||= nil -%>
6
+ <% if attribute.password_digest? -%>
7
+ t.string :password_digest<%= attribute.inject_options %>
8
+ <% elsif attribute.token? -%>
9
+ t.string :<%= attribute.name %><%= attribute.inject_options %>
10
+ <% elsif attribute.reference? -%>
11
+ t.<%= attribute.type %> :<%= attribute.name %><%= attribute.inject_options %>
12
+ <% elsif !attribute.virtual? -%>
13
+ t.<%= attribute.type %> :<%= attribute.name %><%= attribute.inject_options %>
14
+ <% end -%>
15
+ <% end -%>
16
+ <% if options[:timestamps] %>
17
+ t.audits
18
+ <% end -%>
19
+ end
20
+ <% attributes.select(&:token?).each do |attribute| -%>
21
+ add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>, unique: true
22
+ <% end -%>
23
+ <% attributes_with_index.each do |attribute| -%>
24
+ add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>
25
+ <% end -%>
26
+ end
27
+ end
@@ -0,0 +1,7 @@
1
+ require "rails/generators/active_record/model/model_generator"
2
+ require "olivander/generators/olivander/active_record_generator"
3
+
4
+ module Olivander
5
+ class ModelGenerator < ::Olivander::ActiveRecordGenerator
6
+ end
7
+ end
@@ -0,0 +1,117 @@
1
+ module Olivander
2
+ class ScaffoldGenerator < Rails::Generators::NamedBase
3
+ include Rails::Generators::ResourceHelpers
4
+
5
+ argument :attributes, type: :array, default: [], banner: "field[:type][:index] field[:type][:index]"
6
+ class_option :menu,
7
+ type: :boolean,
8
+ default: true,
9
+ desc: "Modify MenuBuilder to include menu item"
10
+
11
+ source_root File.expand_path('templates/scaffold', __dir__)
12
+
13
+ hook_for :orm, as: :model, required: true do |instance, controller|
14
+ instance.invoke controller, [ instance.name ], instance.options.merge({ test_framework: false })
15
+ end
16
+
17
+ hook_for :datatable, type: :boolean, default: true
18
+
19
+ hook_for :resource_controller do |instance, controller|
20
+ instance.invoke controller, [ instance.name.pluralize ], instance.options.merge({ helper: false, test_framework: false, assets: false })
21
+ end
22
+
23
+ def create_views
24
+ # template "views/_form.html.haml", File.join("app/views", controller_file_path, '_form.html.haml')
25
+ # template "views/_model.html.haml", File.join("app/views", controller_file_path, "_#{file_name}.html.haml")
26
+ end
27
+
28
+ # Make an entry in \Rails routing file <tt>config/routes.rb</tt>
29
+ #
30
+ # route "root 'welcome#index'"
31
+ # route "root 'admin#index'", namespace: :admin
32
+ def handle_route
33
+ return if options[:actions].present?
34
+
35
+ use_route_builder = File.exist?(Rails.root.join("app/services/route_builder.rb"))
36
+ if use_route_builder
37
+ route_for_route_builder
38
+ else
39
+ route "resources :#{file_name.pluralize}", namespace: regular_class_path
40
+ end
41
+ end
42
+
43
+ def handle_menu
44
+ say_status :menu, "Handling menu option", :yellow
45
+ return unless options[:menu]
46
+
47
+ target = 'app/services/menu_builder.rb'
48
+ unless File.exist?(target)
49
+ say_status :error, "#{target} not found, skipping menu injection", :red
50
+ return
51
+ end
52
+
53
+ # The exact line we want to insert (note trailing comma)
54
+ new_line = <<~RUBY.indent(6)
55
+ builder.build_menu_item(key: "#{file_name.pluralize}", url: builder.#{file_name.pluralize}_path),
56
+ RUBY
57
+
58
+ file_contents = File.read(target)
59
+ # avoid inserting duplicates
60
+ if file_contents.include?(new_line.strip)
61
+ say_status :skip, "menu item already present in #{target}", :yellow
62
+ return
63
+ end
64
+
65
+ # Insert before the closing bracket of the array (a line that only contains optional whitespace and `]`)
66
+ inject_into_file target,
67
+ new_line,
68
+ before: /^\s*\]\s*$/m,
69
+ verbose: true,
70
+ force: false
71
+
72
+ say_status :done, "Inserted menu item into #{target}", :green
73
+ end
74
+
75
+ private
76
+
77
+ def route_for_route_builder
78
+ route 'RouteBuilder.build_routes(self)'
79
+ namespace = regular_class_path
80
+ namespace = Array(namespace)
81
+ routing_code = "resource :#{file_name.pluralize}, namespaces:[#{namespace.map{ |n| ":#{n}" }.join(', ')}]"
82
+ namespace_pattern = namespace.each_with_index.reverse_each.reduce(nil) do |pattern, (name, i)|
83
+ cummulative_margin = "\\#{i + 1}[ ]{2}"
84
+ blank_or_indented_line = "^[ ]*\n|^#{cummulative_margin}.*\n"
85
+ "(?:(?:#{blank_or_indented_line})*?^(#{cummulative_margin})namespace :#{name} do\n#{pattern})?"
86
+ end.then do |pattern|
87
+ /^([ ]*).+include Olivander::Resources::RouteBuilder*\n#{pattern}/
88
+ end
89
+ # routing_code = namespace.reverse.reduce(routing_code) do |code, name|
90
+ # "namespace :#{name} do\n#{rebase_indentation(code, 2)}end"
91
+ # end
92
+
93
+ log :route, routing_code
94
+
95
+ target_file_name = "app/services/route_builder.rb"
96
+ in_root do
97
+ if namespace_match = match_file(target_file_name, namespace_pattern)
98
+ base_indent, *, existing_block_indent = namespace_match.captures.compact.map(&:length)
99
+ existing_line_pattern = /^[ ]{,#{existing_block_indent}}\S.+\n?/
100
+ routing_code = rebase_indentation(routing_code, base_indent + 1).gsub(existing_line_pattern, "")
101
+ namespace_pattern = /#{Regexp.escape namespace_match.to_s}/
102
+ end
103
+
104
+ inject_into_file target_file_name, routing_code, after: namespace_pattern, verbose: true, force: false
105
+
106
+ if behavior == :revoke && namespace.any? && namespace_match
107
+ empty_block_pattern = /(#{namespace_pattern})((?:\s*end\n){1,#{namespace.size}})/
108
+ gsub_file target_file_name, empty_block_pattern, verbose: false, force: true do |matched|
109
+ beginning, ending = empty_block_pattern.match(matched).captures
110
+ ending.sub!(/\A\s*end\n/, "") while !ending.empty? && beginning.sub!(/^[ ]*namespace .+ do\n\s*\z/, "")
111
+ beginning + ending
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,29 @@
1
+ <% module_namespacing do -%>
2
+ # frozen_string_literal: true
3
+
4
+ # Datatable class for <%= plural_table_name %> table
5
+ class <%= class_name %>Datatable < ::Olivander::Datatable
6
+ auto_datatable <%= class_name %>
7
+ # only: %w[],
8
+ # hide: %w[],
9
+ # show: %w[],
10
+ # scopes: %i[],
11
+ # collection: <%= class_name %>.all
12
+
13
+ # filters do
14
+ # scope :all, default: true
15
+ # end
16
+
17
+ # datatable do
18
+ # order :id, :desc
19
+ <% attributes.each do |attribute| -%>
20
+ # col :<%= attribute.name %>
21
+ <% end -%>
22
+ # actions_col
23
+ # end
24
+
25
+ # collection do
26
+ # <%= class_name %>.all
27
+ # end
28
+ end
29
+ <% end -%>
@@ -0,0 +1,16 @@
1
+ class ContextBuilder
2
+ include Rails.application.routes.url_helpers
3
+
4
+ def self.build_context
5
+ builder = ContextBuilder.new
6
+ Olivander::CurrentContext.build do |_ctx, app_ctx, _user, _ability|
7
+ app_ctx.name = 'Set Your System Name'
8
+ app_ctx.logo.url = '/images/logo.png'
9
+ app_ctx.logo.alt = 'Set Your Logo Alt Text'
10
+ app_ctx.company.name = 'Set Your Company Name'
11
+ app_ctx.menu_items = MenuBuilder.build_menu_items
12
+ app_ctx.route_builder = RouteBuilder
13
+ app_ctx.sign_out_path = builder.root_path
14
+ end.application_context
15
+ end
16
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # a default landing page for Olivander
4
+ class HelloOlivanderController < ApplicationController
5
+ layout 'olivander/adminlte/main'
6
+
7
+ def index
8
+ @page_title = 'Welcome to Olivander'
9
+ render({html: "This is only the beginning...".html_safe, layout: true})
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ //= link_tree ../images
2
+ //= link_directory ../stylesheets .css
3
+ //= link_tree ../../javascript .js
4
+ //= link_tree ../../../vendor/javascript .js
5
+ //= link olivander_manifest.js
@@ -0,0 +1,32 @@
1
+ class MenuBuilder
2
+ include Rails.application.routes.url_helpers
3
+
4
+ def self.build_menu_items
5
+ builder = MenuBuilder.new
6
+ [
7
+ builder.build_menu_item(key: 'root', url: builder.root_path),
8
+ ]
9
+ end
10
+
11
+ def condition_proc(verb, noun)
12
+ proc { Olivander::CurrentContext.ability.can? verb, noun }
13
+ end
14
+
15
+ def build_menu_item(key: nil, controller: nil, action: nil, url: nil, is_module: false, condition: nil, items: nil)
16
+ url ||= url_for(controller:, action:, only_path: true)
17
+ key ||= url.gsub('.', '_').gsub(' ', '_')
18
+
19
+ condition = proc { true } if !condition && is_module
20
+
21
+ unless items
22
+ submenu_key = "#{key.gsub('.', '_')}_submenu"
23
+ items = proc { send(submenu_key) } if respond_to?(submenu_key)
24
+ end
25
+
26
+ key = key.gsub('help.', '') if !I18n.exists?("menus.#{key}") && key.starts_with?('help.')
27
+ mi = Olivander::Menus::MenuItem.new(key, url, nil, is_module:)
28
+ mi.with_condition { condition.is_a?(Proc) ? condition.call : condition } if condition.present?
29
+ mi.with_submenu_items { items.call } if items.present?
30
+ mi
31
+ end
32
+ end
@@ -0,0 +1,8 @@
1
+ <% module_namespacing do -%>
2
+ # frozen_string_literal: true
3
+
4
+ # RouteBuilder for Olivander
5
+ class RouteBuilder
6
+ include Olivander::Resources::RouteBuilder
7
+ end
8
+ <% end %>
@@ -2,5 +2,5 @@
2
2
 
3
3
  # needed for gem
4
4
  module Olivander
5
- VERSION = '0.2.0.49'
5
+ VERSION = '0.2.0.51'
6
6
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: five-two-nw-olivander
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0.49
4
+ version: 0.2.0.51
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eric Dennis
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-07-16 00:00:00.000000000 Z
11
+ date: 2025-10-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: chartkick
@@ -310,6 +310,22 @@ files:
310
310
  - config/locales/simple_form.en.yml
311
311
  - config/routes.rb
312
312
  - lib/five-two-nw-olivander.rb
313
+ - lib/generators/olivander/active_record/model/templates/model.rb.tt
314
+ - lib/generators/olivander/active_record/model/templates/module.rb.tt
315
+ - lib/generators/olivander/active_record_generator.rb
316
+ - lib/generators/olivander/controller/templates/controller.rb.tt
317
+ - lib/generators/olivander/controller_generator.rb
318
+ - lib/generators/olivander/datatable_generator.rb
319
+ - lib/generators/olivander/infrastructure_generator.rb
320
+ - lib/generators/olivander/migration/templates/create_table_migration.rb.tt
321
+ - lib/generators/olivander/model_generator.rb
322
+ - lib/generators/olivander/scaffold_generator.rb
323
+ - lib/generators/olivander/templates/datatable/datatable.rb.tt
324
+ - lib/generators/olivander/templates/infrastructure/context_builder.rb.tt
325
+ - lib/generators/olivander/templates/infrastructure/hello_olivander_controller.rb.tt
326
+ - lib/generators/olivander/templates/infrastructure/manifest.js
327
+ - lib/generators/olivander/templates/infrastructure/menu_builder.rb.tt
328
+ - lib/generators/olivander/templates/infrastructure/route_builder.rb.tt
313
329
  - lib/o18n.rb
314
330
  - lib/olivander.rb
315
331
  - lib/olivander/application_context.rb