madmin 1.0.0.beta2 → 1.2.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.
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: