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 +4 -4
- data/README.md +4 -1
- data/app/controllers/madmin/resource_controller.rb +1 -1
- data/app/views/madmin/application/_javascript.html.erb +36 -1
- data/app/views/madmin/fields/nested_has_many/_fields.html.erb +18 -0
- data/app/views/madmin/fields/nested_has_many/_form.html.erb +30 -0
- data/app/views/madmin/fields/nested_has_many/_index.html.erb +1 -0
- data/app/views/madmin/fields/nested_has_many/_show.html.erb +5 -0
- data/app/views/madmin/fields/password/_form.html.erb +2 -0
- data/app/views/madmin/fields/password/_index.html.erb +1 -0
- data/app/views/madmin/fields/password/_show.html.erb +1 -0
- data/lib/generators/madmin/resource/resource_generator.rb +14 -3
- data/lib/generators/madmin/resource/templates/resource.rb.tt +5 -0
- data/lib/generators/madmin/views/javascript_generator.rb +15 -0
- data/lib/generators/madmin/views/views_generator.rb +6 -5
- data/lib/madmin.rb +11 -9
- data/lib/madmin/fields/belongs_to.rb +3 -2
- data/lib/madmin/fields/has_many.rb +3 -2
- data/lib/madmin/fields/nested_has_many.rb +40 -0
- data/lib/madmin/fields/password.rb +6 -0
- data/lib/madmin/resource.rb +31 -7
- data/lib/madmin/version.rb +1 -1
- metadata +12 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 223251f99312a051f3fd6a6d926c6583347e2c78b255343bc66b3fb063ea8a54
|
4
|
+
data.tar.gz: cfab141916431f52d137bd7704c33cc4cd3b1781735062e2a9d173f9d9d7bee4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
+

|
14
|
+
_We're still working on the design!_
|
15
|
+
|
13
16
|
## Installation
|
14
17
|
Add `madmin` to your application's Gemfile:
|
15
18
|
|
@@ -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 @@
|
|
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
|
-
#
|
102
|
-
elsif
|
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
|
-
|
8
|
-
call_generator("
|
9
|
-
call_generator("
|
10
|
-
call_generator("
|
11
|
-
call_generator("
|
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 :
|
28
|
-
autoload :
|
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
|
-
|
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
|
-
|
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
|
data/lib/madmin/resource.rb
CHANGED
@@ -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
|
-
|
42
|
-
|
43
|
-
|
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
|
-
"
|
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
|
-
"
|
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
|
-
"
|
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
|
data/lib/madmin/version.rb
CHANGED
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
|
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-
|
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
|