madmin 1.0.0.beta2 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +48 -1
  3. data/app/controllers/madmin/resource_controller.rb +4 -2
  4. data/app/helpers/madmin/sort_helper.rb +31 -0
  5. data/app/views/madmin/application/_javascript.html.erb +40 -5
  6. data/app/views/madmin/application/index.html.erb +1 -1
  7. data/app/views/madmin/fields/belongs_to/_index.html.erb +3 -2
  8. data/app/views/madmin/fields/belongs_to/_show.html.erb +3 -2
  9. data/app/views/madmin/fields/enum/_form.html.erb +1 -2
  10. data/app/views/madmin/fields/nested_has_many/_fields.html.erb +18 -0
  11. data/app/views/madmin/fields/nested_has_many/_form.html.erb +30 -0
  12. data/app/views/madmin/fields/nested_has_many/_index.html.erb +1 -0
  13. data/app/views/madmin/fields/nested_has_many/_show.html.erb +5 -0
  14. data/app/views/madmin/fields/password/_form.html.erb +2 -0
  15. data/app/views/madmin/fields/password/_index.html.erb +1 -0
  16. data/app/views/madmin/fields/password/_show.html.erb +1 -0
  17. data/lib/generators/madmin/resource/resource_generator.rb +14 -3
  18. data/lib/generators/madmin/resource/templates/resource.rb.tt +14 -0
  19. data/lib/generators/madmin/views/edit_generator.rb +16 -0
  20. data/lib/generators/madmin/views/form_generator.rb +15 -0
  21. data/lib/generators/madmin/views/index_generator.rb +15 -0
  22. data/lib/generators/madmin/views/javascript_generator.rb +15 -0
  23. data/lib/generators/madmin/views/layout_generator.rb +21 -0
  24. data/lib/generators/madmin/views/navigation_generator.rb +15 -0
  25. data/lib/generators/madmin/views/new_generator.rb +16 -0
  26. data/lib/generators/madmin/views/show_generator.rb +15 -0
  27. data/lib/generators/madmin/views/views_generator.rb +16 -0
  28. data/lib/madmin.rb +11 -9
  29. data/lib/madmin/fields/belongs_to.rb +3 -2
  30. data/lib/madmin/fields/enum.rb +3 -0
  31. data/lib/madmin/fields/has_many.rb +3 -2
  32. data/lib/madmin/fields/nested_has_many.rb +40 -0
  33. data/lib/madmin/fields/password.rb +6 -0
  34. data/lib/madmin/namespace.rb +35 -0
  35. data/lib/madmin/resource.rb +42 -9
  36. data/lib/madmin/version.rb +1 -1
  37. data/lib/madmin/view_generator.rb +42 -0
  38. metadata +25 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5e0427cd7d7dd77a86ef9b7cfd6b08af93cc2f6f542a7992b69d63328c483e26
4
- data.tar.gz: f85af965580b168e7efa890049ca58a46d9710ab3484ed99b45acf7267a7859c
3
+ metadata.gz: e868d17abd508729232e7ebeebd4b79a8e69545b853276a700c60e61e5d6348c
4
+ data.tar.gz: b19043a0a2e7dd0ad6392f995b76812b434e54f73682eb163ca28e1aaeb4ad34
5
5
  SHA512:
6
- metadata.gz: b63ca0956bd1a730d686b7f35252e951f0bb88743df2a2d9fa025120a82647a141e1e20dae3052f889e4d1db7bbf69328ab26e1285d1c602a58b0e625f084eed
7
- data.tar.gz: 11783c59ee3445c594dd2ce51ea5b24ee5942d59f59a2ae6f444ff8991f353e298473f6dfcf8e38c923248458dd38957cd3d8aead6e4c18d1821abcce023a10e
6
+ metadata.gz: '09b7ec920a1c8ae8a1429db77cff71ca45574e379563db5a06648d5cd5434950597b99eed5b836f7834cdc5c71e9d68411f064cc78d363da848efde9a0ef1b1a'
7
+ data.tar.gz: 51b2b25ddabe18e8777c9eab9e34dd1b8afd74a05aae5671f2f94c85cb404043bcde8423d996033a9dd4fa07ee672ad36240e65e195166586afabc02d756237a
data/README.md CHANGED
@@ -7,9 +7,12 @@
7
7
  Why another Ruby on Rails admin? We wanted an admin that was:
8
8
 
9
9
  * Familiar and customizable like Rails scaffolds (less DSL)
10
- * Supports all the Rails features out of the box (ActionText, ActionMailbox, etc)
10
+ * Supports all the Rails features out of the box (ActionText, ActionMailbox, has_secure_password, etc)
11
11
  * Stimulus / Turbolinks / Hotwire ready
12
12
 
13
+ ![Madmin Screenshot](docs/images/screenshot.png)
14
+ _We're still working on the design!_
15
+
13
16
  ## Installation
14
17
  Add `madmin` to your application's Gemfile:
15
18
 
@@ -37,6 +40,50 @@ To generate a resource for a model, you can run:
37
40
  rails g madmin:resource ActionText::RichText
38
41
  ```
39
42
 
43
+ ## Configuring Views
44
+
45
+ The views packaged within the gem are a great starting point, but inevitably people will need to be able to customize those views.
46
+
47
+ You can use the included generator to create the appropriate view files, which can then be customized.
48
+
49
+ For example, running the following will copy over all of the views into your application that will be used for every resource:
50
+ ```bash
51
+ rails generate madmin:views
52
+ ```
53
+
54
+ The view files that are copied over in this case includes all of the standard Rails action views (index, new, edit, show, and _form), as well as:
55
+ * `application.html.erb` (layout file)
56
+ * `_javascript.html.erb` (default JavaScript setup)
57
+ * `_navigation.html.erb` (renders the navigation/sidebar menu)
58
+
59
+ As with the other views, you can specifically run the views generator for only the navigation or application layout views:
60
+ ```bash
61
+ rails g madmin:views:navigation
62
+ # -> app/views/madmin/_navigation.html.erb
63
+
64
+ rails g madmin:views:layout # Note the layout generator includes the layout, javascript, and navigation files.
65
+ # -> app/views/madmin/application.html.erb
66
+ # -> app/views/madmin/_javascript.html.erb
67
+ # -> app/views/madmin/_navigation.html.erb
68
+ ```
69
+
70
+ If you only need to customize specific views, you can restrict which views are copied by the generator:
71
+ ```bash
72
+ rails g madmin:views:index
73
+ # -> app/views/madmin/application/index.html.erb
74
+ ```
75
+
76
+ You can also scope the copied view(s) to a specific Resource/Model:
77
+ ```bash
78
+ rails generate madmin:views:index Book
79
+ # -> app/views/madmin/books/index.html.erb
80
+ ```
81
+
82
+ ## Authentication
83
+
84
+ You can use a couple of strategies to authenticate users who are trying to
85
+ access your madmin panel: [Authentication Docs](docs/authentication.md)
86
+
40
87
  ## 🙏 Contributing
41
88
 
42
89
  This project uses Standard for formatting Ruby code. Please make sure to run standardrb before submitting pull requests.
@@ -1,5 +1,7 @@
1
1
  module Madmin
2
2
  class ResourceController < ApplicationController
3
+ include SortHelper
4
+
3
5
  before_action :set_record, except: [:index, :new, :create]
4
6
 
5
7
  def index
@@ -41,7 +43,7 @@ module Madmin
41
43
  private
42
44
 
43
45
  def set_record
44
- @record = resource.model.find(params[:id])
46
+ @record = resource.model_find(params[:id])
45
47
  end
46
48
 
47
49
  def resource
@@ -54,7 +56,7 @@ module Madmin
54
56
  end
55
57
 
56
58
  def scoped_resources
57
- resource.model.send(valid_scope)
59
+ resource.model.send(valid_scope).order(sort_column => sort_direction)
58
60
  end
59
61
 
60
62
  def valid_scope
@@ -0,0 +1,31 @@
1
+ module Madmin
2
+ module SortHelper
3
+ def sortable(column, title, options = {})
4
+ matching_column = (column.to_s == sort_column)
5
+
6
+ link_to request.params.merge(sort: column, direction: sort_direction), options do
7
+ concat title
8
+ if matching_column
9
+ concat " "
10
+ concat tag.i(sort_direction == "asc" ? "▲" : "▼")
11
+ end
12
+ end
13
+ end
14
+
15
+ def sort_column
16
+ resource.sortable_columns.include?(params[:sort]) ? params[:sort] : default_sort_column
17
+ end
18
+
19
+ def sort_direction
20
+ ["asc", "desc"].include?(params[:direction]) ? params[:direction] : default_sort_direction
21
+ end
22
+
23
+ def default_sort_column
24
+ resource.try(:default_sort_column) || "created_at"
25
+ end
26
+
27
+ def default_sort_direction
28
+ resource.try(:default_sort_direction) || "desc"
29
+ end
30
+ end
31
+ end
@@ -1,9 +1,9 @@
1
- <%= stylesheet_link_tag "https://cdn.skypack.dev/flatpickr/dist/flatpickr.min.css", "data-turbo-track": "reload" %>
2
- <%= stylesheet_link_tag "https://cdn.skypack.dev/slim-select/dist/slimselect.min.css", "data-turbo-track": "reload" %>
3
- <%= stylesheet_link_tag "https://cdn.skypack.dev/trix/dist/trix.css", "data-turbo-track": "reload" %>
1
+ <%= stylesheet_link_tag "https://unpkg.com/flatpickr/dist/flatpickr.min.css", "data-turbo-track": "reload" %>
2
+ <%= stylesheet_link_tag "https://unpkg.com/trix/dist/trix.css", "data-turbo-track": "reload" %>
3
+ <%= stylesheet_link_tag "https://unpkg.com/slim-select@1.27.0/dist/slimselect.min.css", "data-turbo-track": "reload" %>
4
4
 
5
5
  <script type="module">
6
- import { Application } from 'https://cdn.skypack.dev/stimulus'
6
+ import { Application, Controller } from 'https://cdn.skypack.dev/stimulus'
7
7
  const application = Application.start()
8
8
 
9
9
  import stimulusFlatpickr from 'https://cdn.skypack.dev/stimulus-flatpickr'
@@ -17,8 +17,43 @@
17
17
  import 'https://cdn.skypack.dev/trix'
18
18
  import 'https://cdn.skypack.dev/@rails/actiontext@<%= npm_rails_version %>'
19
19
 
20
- Rails.start()
20
+ if (!window._rails_loaded) { Rails.start() }
21
21
  ActiveStorage.start()
22
22
 
23
23
  import * as Turbo from "https://cdn.skypack.dev/@hotwired/turbo"
24
+
25
+ (() => {
26
+ application.register('nested-form', class extends Controller {
27
+ static get targets() {
28
+ return [ "links", "template" ]
29
+ }
30
+
31
+ connect() {
32
+ this.wrapperClass = this.data.get("wrapperClass") || "nested-fields"
33
+ }
34
+
35
+ add_association(event) {
36
+ event.preventDefault()
37
+
38
+ var content = this.templateTarget.innerHTML.replace(/NEW_RECORD/g, new Date().getTime())
39
+ this.linksTarget.insertAdjacentHTML('beforebegin', content)
40
+ }
41
+
42
+ remove_association(event) {
43
+ event.preventDefault()
44
+
45
+ let wrapper = event.target.closest("." + this.wrapperClass)
46
+
47
+ // New records are simply removed from the page
48
+ if (wrapper.dataset.newRecord == "true") {
49
+ wrapper.remove()
50
+
51
+ // Existing records are hidden and flagged for deletion
52
+ } else {
53
+ wrapper.querySelector("input[name*='_destroy']").value = 1
54
+ wrapper.style.display = 'none'
55
+ }
56
+ }
57
+ })
58
+ })()
24
59
  </script>
@@ -20,7 +20,7 @@
20
20
  <% next if attribute[:field].nil? %>
21
21
  <% next unless attribute[:field].visible?(action_name) %>
22
22
 
23
- <th><%= attribute[:name].to_s.titleize %></th>
23
+ <th><%= sortable attribute[:name], attribute[:name].to_s.titleize %></th>
24
24
  <% end %>
25
25
  <th>Actions</th>
26
26
  </tr>
@@ -1,2 +1,3 @@
1
- <% object = field.value(record) %>
2
- <%= link_to Madmin.resource_for(object).display_name(object), Madmin.resource_for(object).show_path(object) %>
1
+ <% if (object = field.value(record)) %>
2
+ <%= link_to Madmin.resource_for(object).display_name(object), Madmin.resource_for(object).show_path(object) %>
3
+ <% end %>
@@ -1,2 +1,3 @@
1
- <% object = field.value(record) %>
2
- <%= link_to Madmin.resource_for(object).display_name(object), Madmin.resource_for(object).show_path(object) %>
1
+ <% if (object = field.value(record)) %>
2
+ <%= link_to Madmin.resource_for(object).display_name(object), Madmin.resource_for(object).show_path(object) %>
3
+ <% end %>
@@ -1,3 +1,2 @@
1
- ENUM
2
1
  <%= form.label field.attribute_name, class: "inline-block w-32 flex-shrink-0" %>
3
- <%= field.value(record) %>
2
+ <%= form.select field.attribute_name, field.options_for_select(record), { prompt: true }, { class: "form-select" } %>
@@ -0,0 +1,18 @@
1
+ <%= content_tag :div, class: "nested-fields bg-gray-100 rounded-t-xl p-5", data: { new_record: f.object.new_record? } do %>
2
+ <% field.nested_attributes.each do |nested_attribute| %>
3
+ <% next if nested_attribute[:field].nil? %>
4
+ <% next unless nested_attribute[:field].visible?(action_name) %>
5
+ <% next unless nested_attribute[:field].visible?(:form) %>
6
+
7
+ <% nested_field = nested_attribute[:field] %>
8
+
9
+ <div class="mb-4 flex">
10
+ <%= render partial: nested_field.to_partial_path("form"), locals: { field: nested_field, record: f.object, form: f, resource: field.resource } %>
11
+ </div>
12
+ <% end %>
13
+
14
+ <small><%= link_to "Remove", "#", data: { action: "click->nested-form#remove_association" } %></small>
15
+
16
+ <%= f.hidden_field :_destroy %>
17
+
18
+ <% end %>
@@ -0,0 +1,30 @@
1
+ <%= form.label field.attribute_name, class: "inline-block w-32 flex-shrink-0" %>
2
+
3
+ <div class="container space-y-8" data-controller="nested-form">
4
+ <template data-target="nested-form.template">
5
+
6
+ <%= form.fields_for field.attribute_name, field.to_model.new, child_index: 'NEW_RECORD' do |nested_form| %>
7
+ <%= render(
8
+ partial: field.to_partial_path('fields'),
9
+ locals: {
10
+ f: nested_form,
11
+ field: field
12
+ }
13
+ ) %>
14
+ <% end %>
15
+ </template>
16
+
17
+ <%= form.fields_for field.attribute_name do |nested_form| %>
18
+ <%= render(
19
+ partial: field.to_partial_path('fields'),
20
+ locals: {
21
+ f: nested_form,
22
+ field: field
23
+ }
24
+ ) %>
25
+ <% end %>
26
+
27
+ <%= content_tag :div, class: '', data: { target:"nested-form.links" } do %>
28
+ <%= link_to "+ Add new", "#", data: { action: "click->nested-form#add_association" } %>
29
+ <% end %>
30
+ </div>
@@ -0,0 +1 @@
1
+ <%= pluralize field.value(record).count, field.attribute_name.to_s %>
@@ -0,0 +1,5 @@
1
+ <% field.value(record).each do |object| %>
2
+ <div>
3
+ <%= link_to Madmin.resource_for(object).display_name(object), Madmin.resource_for(object).show_path(object) %>
4
+ </div>
5
+ <% end %>
@@ -0,0 +1,2 @@
1
+ <%= form.label field.attribute_name, class: "inline-block w-32 flex-shrink-0" %>
2
+ <%= form.password_field field.attribute_name, class: "form-input" %>
@@ -0,0 +1 @@
1
+ <%= field.value(record) %>
@@ -0,0 +1 @@
1
+ <%= field.value(record) %>
@@ -46,6 +46,10 @@ module Madmin
46
46
  def virtual_attributes
47
47
  virtual = []
48
48
 
49
+ # has_secure_password columns
50
+ password_attributes = model.attribute_types.keys.select { |k| k.ends_with?("_digest") }.map { |k| k.delete_suffix("_digest") }
51
+ virtual += password_attributes.map { |attr| [attr, "#{attr}_confirmation"] }.flatten
52
+
49
53
  # Add virtual attributes for ActionText and ActiveStorage
50
54
  model.reflections.each do |name, association|
51
55
  if name.starts_with?("rich_text")
@@ -63,6 +67,9 @@ module Madmin
63
67
  def redundant_attributes
64
68
  redundant = []
65
69
 
70
+ # has_secure_password columns
71
+ redundant += model.attribute_types.keys.select { |k| k.ends_with?("_digest") }
72
+
66
73
  model.reflections.each do |name, association|
67
74
  if association.has_one?
68
75
  next
@@ -98,13 +105,17 @@ module Madmin
98
105
  if %w[id created_at updated_at].include?(name)
99
106
  {form: false}
100
107
 
101
- # Attributes without a database column
102
- elsif !model.column_names.include?(name)
103
- {index: false}
108
+ # has_secure_passwords should only show on forms
109
+ elsif name.ends_with?("_confirmation") || virtual_attributes.include?("#{name}_confirmation")
110
+ {index: false, show: false}
104
111
 
105
112
  # Counter cache columns are typically not editable
106
113
  elsif name.ends_with?("_count")
107
114
  {form: false}
115
+
116
+ # Attributes without a database column
117
+ elsif !model.column_names.include?(name)
118
+ {index: false}
108
119
  end
109
120
  end
110
121
  end
@@ -8,4 +8,18 @@ class <%= class_name %>Resource < Madmin::Resource
8
8
  <% associations.each do |association_name| -%>
9
9
  attribute :<%= association_name %>
10
10
  <% end -%>
11
+
12
+ # Uncomment this to customize the display name of records in the admin area.
13
+ # def self.display_name(record)
14
+ # record.name
15
+ # end
16
+
17
+ # Uncomment this to customize the default sort column and direction.
18
+ # def self.default_sort_column
19
+ # "created_at"
20
+ # end
21
+ #
22
+ # def self.default_sort_direction
23
+ # "desc"
24
+ # end
11
25
  end
@@ -0,0 +1,16 @@
1
+ require "madmin/view_generator"
2
+
3
+ module Madmin
4
+ module Generators
5
+ module Views
6
+ class EditGenerator < Madmin::ViewGenerator
7
+ source_root template_source_path
8
+
9
+ def copy_edit
10
+ copy_resource_template("edit")
11
+ copy_resource_template("_form")
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,15 @@
1
+ require "madmin/view_generator"
2
+
3
+ module Madmin
4
+ module Generators
5
+ module Views
6
+ class FormGenerator < Madmin::ViewGenerator
7
+ source_root template_source_path
8
+
9
+ def copy_form
10
+ copy_resource_template("_form")
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ require "madmin/view_generator"
2
+
3
+ module Madmin
4
+ module Generators
5
+ module Views
6
+ class IndexGenerator < Madmin::ViewGenerator
7
+ source_root template_source_path
8
+
9
+ def copy_template
10
+ copy_resource_template("index")
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ require "madmin/view_generator"
2
+
3
+ module Madmin
4
+ module Generators
5
+ module Views
6
+ class JavascriptGenerator < Madmin::ViewGenerator
7
+ source_root template_source_path
8
+
9
+ def copy_navigation
10
+ copy_resource_template("_javascript")
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,21 @@
1
+ require "madmin/view_generator"
2
+
3
+ module Madmin
4
+ module Generators
5
+ module Views
6
+ class LayoutGenerator < Madmin::ViewGenerator
7
+ source_root template_source_path
8
+
9
+ def copy_template
10
+ copy_file(
11
+ "../../layouts/madmin/application.html.erb",
12
+ "app/views/layouts/madmin/application.html.erb"
13
+ )
14
+
15
+ call_generator("madmin:views:navigation")
16
+ copy_resource_template("_javascript")
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,15 @@
1
+ require "madmin/view_generator"
2
+
3
+ module Madmin
4
+ module Generators
5
+ module Views
6
+ class NavigationGenerator < Madmin::ViewGenerator
7
+ source_root template_source_path
8
+
9
+ def copy_navigation
10
+ copy_resource_template("_navigation")
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,16 @@
1
+ require "madmin/view_generator"
2
+
3
+ module Madmin
4
+ module Generators
5
+ module Views
6
+ class NewGenerator < Madmin::ViewGenerator
7
+ source_root template_source_path
8
+
9
+ def copy_new
10
+ copy_resource_template("new")
11
+ copy_resource_template("_form")
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,15 @@
1
+ require "madmin/view_generator"
2
+
3
+ module Madmin
4
+ module Generators
5
+ module Views
6
+ class ShowGenerator < Madmin::ViewGenerator
7
+ source_root template_source_path
8
+
9
+ def copy_template
10
+ copy_resource_template("show")
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,16 @@
1
+ require "madmin/view_generator"
2
+
3
+ module Madmin
4
+ module Generators
5
+ class ViewsGenerator < Madmin::ViewGenerator
6
+ def copy_templates
7
+ # Some generators duplicate templates, so not everything is present here
8
+ call_generator("madmin:views:edit", resource_path, "--namespace", namespace)
9
+ call_generator("madmin:views:index", resource_path, "--namespace", namespace)
10
+ call_generator("madmin:views:layout", resource_path, "--namespace", namespace)
11
+ call_generator("madmin:views:new", resource_path, "--namespace", namespace)
12
+ call_generator("madmin:views:show", resource_path, "--namespace", namespace)
13
+ end
14
+ end
15
+ end
16
+ end
data/lib/madmin.rb CHANGED
@@ -8,24 +8,26 @@ module Madmin
8
8
  autoload :Resource, "madmin/resource"
9
9
 
10
10
  module Fields
11
+ autoload :Attachment, "madmin/fields/attachment"
12
+ autoload :Attachments, "madmin/fields/attachments"
13
+ autoload :BelongsTo, "madmin/fields/belongs_to"
11
14
  autoload :Boolean, "madmin/fields/boolean"
12
- autoload :Integer, "madmin/fields/integer"
13
- autoload :String, "madmin/fields/string"
14
- autoload :Text, "madmin/fields/text"
15
15
  autoload :Date, "madmin/fields/date"
16
16
  autoload :DateTime, "madmin/fields/date_time"
17
17
  autoload :Decimal, "madmin/fields/decimal"
18
- autoload :Json, "madmin/fields/json"
19
18
  autoload :Enum, "madmin/fields/enum"
20
19
  autoload :Float, "madmin/fields/float"
21
- autoload :Time, "madmin/fields/time"
22
- autoload :BelongsTo, "madmin/fields/belongs_to"
23
- autoload :Polymorphic, "madmin/fields/polymorphic"
24
20
  autoload :HasMany, "madmin/fields/has_many"
25
21
  autoload :HasOne, "madmin/fields/has_one"
22
+ autoload :Integer, "madmin/fields/integer"
23
+ autoload :Json, "madmin/fields/json"
24
+ autoload :NestedHasMany, "madmin/fields/nested_has_many"
25
+ autoload :Password, "madmin/fields/password"
26
+ autoload :Polymorphic, "madmin/fields/polymorphic"
26
27
  autoload :RichText, "madmin/fields/rich_text"
27
- autoload :Attachment, "madmin/fields/attachment"
28
- autoload :Attachments, "madmin/fields/attachments"
28
+ autoload :String, "madmin/fields/string"
29
+ autoload :Text, "madmin/fields/text"
30
+ autoload :Time, "madmin/fields/time"
29
31
  end
30
32
 
31
33
  mattr_accessor :resources, default: []
@@ -3,10 +3,11 @@ module Madmin
3
3
  class BelongsTo < Field
4
4
  def options_for_select(record)
5
5
  association = record.class.reflect_on_association(attribute_name)
6
-
7
6
  klass = association.klass
7
+ resource = nil
8
8
  klass.all.map do |r|
9
- ["#{klass.name} ##{r.id}", r.id]
9
+ resource ||= Madmin.resource_for(r)
10
+ [resource.display_name(r), r.id]
10
11
  end
11
12
  end
12
13
 
@@ -1,6 +1,9 @@
1
1
  module Madmin
2
2
  module Fields
3
3
  class Enum < Field
4
+ def options_for_select(record)
5
+ model.defined_enums[attribute_name.to_s].keys
6
+ end
4
7
  end
5
8
  end
6
9
  end
@@ -3,10 +3,11 @@ module Madmin
3
3
  class HasMany < Field
4
4
  def options_for_select(record)
5
5
  association = record.class.reflect_on_association(attribute_name)
6
-
7
6
  klass = association.klass
7
+ resource = nil
8
8
  klass.all.map do |r|
9
- ["#{klass.name} ##{r.id}", r.id]
9
+ resource ||= Madmin.resource_for(r)
10
+ [resource.display_name(r), r.id]
10
11
  end
11
12
  end
12
13
 
@@ -0,0 +1,40 @@
1
+ module Madmin
2
+ module Fields
3
+ class NestedHasMany < Field
4
+ DEFAULT_ATTRIBUTES = %w[_destroy id].freeze
5
+ def nested_attributes
6
+ resource.attributes.reject { |i| skipped_fields.include?(i[:name]) }
7
+ end
8
+
9
+ def resource
10
+ "#{to_model.name}Resource".constantize
11
+ end
12
+
13
+ def to_param
14
+ {"#{attribute_name}_attributes": permitted_fields}
15
+ end
16
+
17
+ def to_partial_path(name)
18
+ unless %w[index show form fields].include? name
19
+ raise ArgumentError, "`partial` must be 'index', 'show', 'form' or 'fields'"
20
+ end
21
+
22
+ "/madmin/fields/#{self.class.field_type}/#{name}"
23
+ end
24
+
25
+ def to_model
26
+ attribute_name.to_s.singularize.classify.constantize
27
+ end
28
+
29
+ private
30
+
31
+ def permitted_fields
32
+ (resource.permitted_params - skipped_fields + DEFAULT_ATTRIBUTES).uniq
33
+ end
34
+
35
+ def skipped_fields
36
+ options[:skip] || []
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,6 @@
1
+ module Madmin
2
+ module Fields
3
+ class Password < Field
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,35 @@
1
+ module Madmin
2
+ class Namespace
3
+ def initialize(namespace)
4
+ @namespace = namespace
5
+ end
6
+
7
+ def resources
8
+ @resources ||= routes.map(&:first).uniq.map { |path|
9
+ Resource.new(namespace, path)
10
+ }
11
+ end
12
+
13
+ def routes
14
+ @routes ||= all_routes.select { |controller, _action|
15
+ controller.starts_with?("#{namespace}/")
16
+ }.map { |controller, action|
17
+ [controller.gsub(/^#{namespace}\//, ""), action]
18
+ }
19
+ end
20
+
21
+ def resources_with_index_route
22
+ routes.select { |_resource, route| route == "index" }.map(&:first).uniq
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :namespace
28
+
29
+ def all_routes
30
+ Rails.application.routes.routes.map do |route|
31
+ route.defaults.values_at(:controller, :action).map(&:to_s)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -18,6 +18,10 @@ module Madmin
18
18
  model_name.constantize
19
19
  end
20
20
 
21
+ def model_find(id)
22
+ friendly_model? ? model.friendly.find(id) : model.find(id)
23
+ end
24
+
21
25
  def model_name
22
26
  to_s.chomp("Resource").classify
23
27
  end
@@ -38,21 +42,27 @@ module Madmin
38
42
  end
39
43
 
40
44
  def index_path(options = {})
41
- path = "/madmin/#{model.model_name.collection}"
42
- path += "?#{options.to_param}" if options.any?
43
- path
45
+ route_name = "madmin_#{model.table_name}_path"
46
+
47
+ url_helpers.send(route_name, options)
44
48
  end
45
49
 
46
50
  def new_path
47
- "/madmin/#{model.model_name.collection}/new"
51
+ route_name = "new_madmin_#{model.model_name.singular}_path"
52
+
53
+ url_helpers.send(route_name)
48
54
  end
49
55
 
50
56
  def show_path(record)
51
- "/madmin/#{model.model_name.collection}/#{record.id}"
57
+ route_name = "madmin_#{model.model_name.singular}_path"
58
+
59
+ url_helpers.send(route_name, record.to_param)
52
60
  end
53
61
 
54
62
  def edit_path(record)
55
- "/madmin/#{model.model_name.collection}/#{record.id}/edit"
63
+ route_name = "edit_madmin_#{model.model_name.singular}_path"
64
+
65
+ url_helpers.send(route_name, record.to_param)
56
66
  end
57
67
 
58
68
  def param_key
@@ -60,13 +70,21 @@ module Madmin
60
70
  end
61
71
 
62
72
  def permitted_params
63
- attributes.map { |a| a[:field].to_param }
73
+ attributes.filter_map { |a| a[:field].to_param if a[:field].visible?(:form) }
64
74
  end
65
75
 
66
76
  def display_name(record)
67
77
  "#{record.class} ##{record.id}"
68
78
  end
69
79
 
80
+ def friendly_model?
81
+ model.respond_to? :friendly
82
+ end
83
+
84
+ def sortable_columns
85
+ model.column_names
86
+ end
87
+
70
88
  private
71
89
 
72
90
  def field_for_type(name, type)
@@ -90,6 +108,7 @@ module Madmin
90
108
  text: Fields::Text,
91
109
  time: Fields::Time,
92
110
  timestamp: Fields::Time,
111
+ password: Fields::Password,
93
112
 
94
113
  # Postgres specific types
95
114
  bit: Fields::String,
@@ -126,7 +145,8 @@ module Madmin
126
145
  polymorphic: Fields::Polymorphic,
127
146
  has_many: Fields::HasMany,
128
147
  has_one: Fields::HasOne,
129
- rich_text: Fields::RichText
148
+ rich_text: Fields::RichText,
149
+ nested_has_many: Fields::NestedHasMany
130
150
  }.fetch(type)
131
151
  rescue
132
152
  raise ArgumentError, <<~MESSAGE
@@ -143,7 +163,12 @@ module Madmin
143
163
  name_string = name.to_s
144
164
 
145
165
  if model.attribute_types.include?(name_string)
146
- model.attribute_types[name_string].type || :string
166
+ column_type = model.attribute_types[name_string]
167
+ if column_type.is_a? ActiveRecord::Enum::EnumType
168
+ :enum
169
+ else
170
+ column_type.type || :string
171
+ end
147
172
  elsif (association = model.reflect_on_association(name))
148
173
  type_for_association(association)
149
174
  elsif model.reflect_on_association(:"rich_text_#{name_string}")
@@ -152,6 +177,10 @@ module Madmin
152
177
  :attachment
153
178
  elsif model.reflect_on_association(:"#{name_string}_attachments")
154
179
  :attachments
180
+
181
+ # has_secure_password
182
+ elsif model.attribute_types.include?("#{name_string}_digest") || name_string.ends_with?("_confirmation")
183
+ :password
155
184
  end
156
185
  end
157
186
 
@@ -166,6 +195,10 @@ module Madmin
166
195
  :belongs_to
167
196
  end
168
197
  end
198
+
199
+ def url_helpers
200
+ @url_helpers ||= Rails.application.routes.url_helpers
201
+ end
169
202
  end
170
203
  end
171
204
  end
@@ -1,3 +1,3 @@
1
1
  module Madmin
2
- VERSION = "1.0.0.beta2"
2
+ VERSION = "1.2.0"
3
3
  end
@@ -0,0 +1,42 @@
1
+ require "rails/generators/base"
2
+ require "madmin/generator_helpers"
3
+ require "madmin/namespace"
4
+
5
+ module Madmin
6
+ class ViewGenerator < Rails::Generators::Base
7
+ include Madmin::GeneratorHelpers
8
+ class_option :namespace, type: :string, default: "madmin"
9
+
10
+ def self.template_source_path
11
+ File.expand_path(
12
+ "../../../app/views/madmin/application",
13
+ __FILE__
14
+ )
15
+ end
16
+
17
+ private
18
+
19
+ def namespace
20
+ options[:namespace]
21
+ end
22
+
23
+ def copy_resource_template(template_name)
24
+ template_file = "#{template_name}.html.erb"
25
+
26
+ copy_file(
27
+ template_file,
28
+ "app/views/#{namespace}/#{resource_path}/#{template_file}"
29
+ )
30
+ end
31
+
32
+ def resource_path
33
+ args.first.try(:underscore).try(:pluralize) || BaseResourcePath.new
34
+ end
35
+
36
+ class BaseResourcePath
37
+ def to_s
38
+ "application"
39
+ end
40
+ end
41
+ end
42
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: madmin
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.beta2
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Oliver
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2021-02-05 00:00:00.000000000 Z
12
+ date: 2021-05-05 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
@@ -63,6 +63,7 @@ files:
63
63
  - app/controllers/madmin/dashboard_controller.rb
64
64
  - app/controllers/madmin/resource_controller.rb
65
65
  - app/helpers/madmin/application_helper.rb
66
+ - app/helpers/madmin/sort_helper.rb
66
67
  - app/views/layouts/madmin/application.html.erb
67
68
  - app/views/madmin/application/_form.html.erb
68
69
  - app/views/madmin/application/_javascript.html.erb
@@ -111,6 +112,13 @@ files:
111
112
  - app/views/madmin/fields/json/_form.html.erb
112
113
  - app/views/madmin/fields/json/_index.html.erb
113
114
  - app/views/madmin/fields/json/_show.html.erb
115
+ - app/views/madmin/fields/nested_has_many/_fields.html.erb
116
+ - app/views/madmin/fields/nested_has_many/_form.html.erb
117
+ - app/views/madmin/fields/nested_has_many/_index.html.erb
118
+ - app/views/madmin/fields/nested_has_many/_show.html.erb
119
+ - app/views/madmin/fields/password/_form.html.erb
120
+ - app/views/madmin/fields/password/_index.html.erb
121
+ - app/views/madmin/fields/password/_show.html.erb
114
122
  - app/views/madmin/fields/polymorphic/_form.html.erb
115
123
  - app/views/madmin/fields/polymorphic/_index.html.erb
116
124
  - app/views/madmin/fields/polymorphic/_show.html.erb
@@ -131,6 +139,15 @@ files:
131
139
  - lib/generators/madmin/resource/resource_generator.rb
132
140
  - lib/generators/madmin/resource/templates/controller.rb.tt
133
141
  - lib/generators/madmin/resource/templates/resource.rb.tt
142
+ - lib/generators/madmin/views/edit_generator.rb
143
+ - lib/generators/madmin/views/form_generator.rb
144
+ - lib/generators/madmin/views/index_generator.rb
145
+ - lib/generators/madmin/views/javascript_generator.rb
146
+ - lib/generators/madmin/views/layout_generator.rb
147
+ - lib/generators/madmin/views/navigation_generator.rb
148
+ - lib/generators/madmin/views/new_generator.rb
149
+ - lib/generators/madmin/views/show_generator.rb
150
+ - lib/generators/madmin/views/views_generator.rb
134
151
  - lib/madmin.rb
135
152
  - lib/madmin/engine.rb
136
153
  - lib/madmin/field.rb
@@ -147,14 +164,18 @@ files:
147
164
  - lib/madmin/fields/has_one.rb
148
165
  - lib/madmin/fields/integer.rb
149
166
  - lib/madmin/fields/json.rb
167
+ - lib/madmin/fields/nested_has_many.rb
168
+ - lib/madmin/fields/password.rb
150
169
  - lib/madmin/fields/polymorphic.rb
151
170
  - lib/madmin/fields/rich_text.rb
152
171
  - lib/madmin/fields/string.rb
153
172
  - lib/madmin/fields/text.rb
154
173
  - lib/madmin/fields/time.rb
155
174
  - lib/madmin/generator_helpers.rb
175
+ - lib/madmin/namespace.rb
156
176
  - lib/madmin/resource.rb
157
177
  - lib/madmin/version.rb
178
+ - lib/madmin/view_generator.rb
158
179
  - lib/tasks/madmin_tasks.rake
159
180
  homepage: https://github.com/excid3/madmin
160
181
  licenses:
@@ -171,9 +192,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
171
192
  version: 2.5.0
172
193
  required_rubygems_version: !ruby/object:Gem::Requirement
173
194
  requirements:
174
- - - ">"
195
+ - - ">="
175
196
  - !ruby/object:Gem::Version
176
- version: 1.3.1
197
+ version: '0'
177
198
  requirements: []
178
199
  rubygems_version: 3.2.3
179
200
  signing_key: