madmin 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 19d2f645bfcae060a5a925b945892a2a196790a2d6c3b296ec0fe5118aeaf186
4
- data.tar.gz: 4310e5b0fb00744f6098032799d6df7368890b736382a9bad466d328662f4da4
3
+ metadata.gz: 223251f99312a051f3fd6a6d926c6583347e2c78b255343bc66b3fb063ea8a54
4
+ data.tar.gz: cfab141916431f52d137bd7704c33cc4cd3b1781735062e2a9d173f9d9d7bee4
5
5
  SHA512:
6
- metadata.gz: c4d157858da05695020064ef9aa636079298cdc7bb1937bc3a4cb13faf71087d31ac0df3d1f3027414112aae61e97f9bfd099a1cf8bbf6bf8380532e654fe844
7
- data.tar.gz: 2969bdf7c7e419290cff1db31f1d22befdb2eb7b0591cc60a00a328dd7e024991825547cae8404a98abdfd309c3308bc7c46332bf154fd6765f48372785baddd
6
+ metadata.gz: 7b1480b7df30ec0bba0f1937a7336bb8532f51cd8dc9c8de78fd73b686c132dd14083eec88a303938bfb4fdfa50827db570a3d899e40952799869dfe4795e756
7
+ data.tar.gz: e362a3d85149b1f1074ea21eaf6a6659e6c8f002a929f668a21583a286fc1824a2f3ede02997eafe75feaa20e0172345c54210180b7c537b44d0de4bca1eb0b0
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
 
@@ -41,7 +41,7 @@ module Madmin
41
41
  private
42
42
 
43
43
  def set_record
44
- @record = resource.model.find(params[:id])
44
+ @record = resource.model_find(params[:id])
45
45
  end
46
46
 
47
47
  def resource
@@ -3,7 +3,7 @@
3
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'
@@ -21,4 +21,39 @@
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>
@@ -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,9 @@ 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
11
16
  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
@@ -4,11 +4,12 @@ module Madmin
4
4
  module Generators
5
5
  class ViewsGenerator < Madmin::ViewGenerator
6
6
  def copy_templates
7
- view = "madmin:views:"
8
- call_generator("#{view}index", resource_path, "--namespace", namespace)
9
- call_generator("#{view}show", resource_path, "--namespace", namespace)
10
- call_generator("#{view}new", resource_path, "--namespace", namespace)
11
- call_generator("#{view}edit", resource_path, "--namespace", namespace)
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)
12
13
  end
13
14
  end
14
15
  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
 
@@ -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
@@ -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
@@ -67,6 +77,10 @@ module Madmin
67
77
  "#{record.class} ##{record.id}"
68
78
  end
69
79
 
80
+ def friendly_model?
81
+ model.respond_to? :friendly
82
+ end
83
+
70
84
  private
71
85
 
72
86
  def field_for_type(name, type)
@@ -90,6 +104,7 @@ module Madmin
90
104
  text: Fields::Text,
91
105
  time: Fields::Time,
92
106
  timestamp: Fields::Time,
107
+ password: Fields::Password,
93
108
 
94
109
  # Postgres specific types
95
110
  bit: Fields::String,
@@ -126,7 +141,8 @@ module Madmin
126
141
  polymorphic: Fields::Polymorphic,
127
142
  has_many: Fields::HasMany,
128
143
  has_one: Fields::HasOne,
129
- rich_text: Fields::RichText
144
+ rich_text: Fields::RichText,
145
+ nested_has_many: Fields::NestedHasMany
130
146
  }.fetch(type)
131
147
  rescue
132
148
  raise ArgumentError, <<~MESSAGE
@@ -157,6 +173,10 @@ module Madmin
157
173
  :attachment
158
174
  elsif model.reflect_on_association(:"#{name_string}_attachments")
159
175
  :attachments
176
+
177
+ # has_secure_password
178
+ elsif model.attribute_types.include?("#{name_string}_digest") || name_string.ends_with?("_confirmation")
179
+ :password
160
180
  end
161
181
  end
162
182
 
@@ -171,6 +191,10 @@ module Madmin
171
191
  :belongs_to
172
192
  end
173
193
  end
194
+
195
+ def url_helpers
196
+ @url_helpers ||= Rails.application.routes.url_helpers
197
+ end
174
198
  end
175
199
  end
176
200
  end
@@ -1,3 +1,3 @@
1
1
  module Madmin
2
- VERSION = "1.0.2"
2
+ VERSION = "1.1.0"
3
3
  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.2
4
+ version: 1.1.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-03-04 00:00:00.000000000 Z
12
+ date: 2021-04-22 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
@@ -111,6 +111,13 @@ files:
111
111
  - app/views/madmin/fields/json/_form.html.erb
112
112
  - app/views/madmin/fields/json/_index.html.erb
113
113
  - app/views/madmin/fields/json/_show.html.erb
114
+ - app/views/madmin/fields/nested_has_many/_fields.html.erb
115
+ - app/views/madmin/fields/nested_has_many/_form.html.erb
116
+ - app/views/madmin/fields/nested_has_many/_index.html.erb
117
+ - app/views/madmin/fields/nested_has_many/_show.html.erb
118
+ - app/views/madmin/fields/password/_form.html.erb
119
+ - app/views/madmin/fields/password/_index.html.erb
120
+ - app/views/madmin/fields/password/_show.html.erb
114
121
  - app/views/madmin/fields/polymorphic/_form.html.erb
115
122
  - app/views/madmin/fields/polymorphic/_index.html.erb
116
123
  - app/views/madmin/fields/polymorphic/_show.html.erb
@@ -134,6 +141,7 @@ files:
134
141
  - lib/generators/madmin/views/edit_generator.rb
135
142
  - lib/generators/madmin/views/form_generator.rb
136
143
  - lib/generators/madmin/views/index_generator.rb
144
+ - lib/generators/madmin/views/javascript_generator.rb
137
145
  - lib/generators/madmin/views/layout_generator.rb
138
146
  - lib/generators/madmin/views/navigation_generator.rb
139
147
  - lib/generators/madmin/views/new_generator.rb
@@ -155,6 +163,8 @@ files:
155
163
  - lib/madmin/fields/has_one.rb
156
164
  - lib/madmin/fields/integer.rb
157
165
  - lib/madmin/fields/json.rb
166
+ - lib/madmin/fields/nested_has_many.rb
167
+ - lib/madmin/fields/password.rb
158
168
  - lib/madmin/fields/polymorphic.rb
159
169
  - lib/madmin/fields/rich_text.rb
160
170
  - lib/madmin/fields/string.rb