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.
- checksums.yaml +4 -4
- data/README.md +48 -1
- data/app/controllers/madmin/resource_controller.rb +4 -2
- data/app/helpers/madmin/sort_helper.rb +31 -0
- data/app/views/madmin/application/_javascript.html.erb +40 -5
- data/app/views/madmin/application/index.html.erb +1 -1
- data/app/views/madmin/fields/belongs_to/_index.html.erb +3 -2
- data/app/views/madmin/fields/belongs_to/_show.html.erb +3 -2
- data/app/views/madmin/fields/enum/_form.html.erb +1 -2
- 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 +14 -0
- data/lib/generators/madmin/views/edit_generator.rb +16 -0
- data/lib/generators/madmin/views/form_generator.rb +15 -0
- data/lib/generators/madmin/views/index_generator.rb +15 -0
- data/lib/generators/madmin/views/javascript_generator.rb +15 -0
- data/lib/generators/madmin/views/layout_generator.rb +21 -0
- data/lib/generators/madmin/views/navigation_generator.rb +15 -0
- data/lib/generators/madmin/views/new_generator.rb +16 -0
- data/lib/generators/madmin/views/show_generator.rb +15 -0
- data/lib/generators/madmin/views/views_generator.rb +16 -0
- data/lib/madmin.rb +11 -9
- data/lib/madmin/fields/belongs_to.rb +3 -2
- data/lib/madmin/fields/enum.rb +3 -0
- 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/namespace.rb +35 -0
- data/lib/madmin/resource.rb +42 -9
- data/lib/madmin/version.rb +1 -1
- data/lib/madmin/view_generator.rb +42 -0
- metadata +25 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e868d17abd508729232e7ebeebd4b79a8e69545b853276a700c60e61e5d6348c
|
4
|
+
data.tar.gz: b19043a0a2e7dd0ad6392f995b76812b434e54f73682eb163ca28e1aaeb4ad34
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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://
|
2
|
-
<%= stylesheet_link_tag "https://
|
3
|
-
<%= stylesheet_link_tag "https://
|
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 %>
|
@@ -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,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 :
|
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
|
|
data/lib/madmin/fields/enum.rb
CHANGED
@@ -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
|
@@ -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
|
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
|
@@ -60,13 +70,21 @@ module Madmin
|
|
60
70
|
end
|
61
71
|
|
62
72
|
def permitted_params
|
63
|
-
attributes.
|
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]
|
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
|
data/lib/madmin/version.rb
CHANGED
@@ -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.
|
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-
|
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:
|
197
|
+
version: '0'
|
177
198
|
requirements: []
|
178
199
|
rubygems_version: 3.2.3
|
179
200
|
signing_key:
|