madmin 1.0.2 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
![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
|
|
@@ -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
|