goodmin 0.0.1
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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +1359 -0
- data/Rakefile +56 -0
- data/app/assets/stylesheets/goodmin/application.css +75 -0
- data/app/controllers/goodmin/application_controller.rb +43 -0
- data/app/controllers/goodmin/resource_controller.rb +235 -0
- data/app/helpers/goodmin/application_helper.rb +45 -0
- data/app/javascript/goodmin/application.js +8 -0
- data/app/javascript/goodmin/controllers/batch_actions_controller.js +101 -0
- data/app/javascript/goodmin/controllers/datetimepicker_controller.js +24 -0
- data/app/javascript/goodmin/controllers/navigation_controller.js +30 -0
- data/app/jobs/goodmin/application_job.rb +4 -0
- data/app/mailers/goodmin/application_mailer.rb +6 -0
- data/app/models/goodmin/application_record.rb +5 -0
- data/app/views/goodmin/application/welcome.html.erb +17 -0
- data/app/views/goodmin/fields/association/_form.html.erb +19 -0
- data/app/views/goodmin/fields/association/_index.html.erb +6 -0
- data/app/views/goodmin/fields/association/_show.html.erb +6 -0
- data/app/views/goodmin/fields/boolean/_form.html.erb +7 -0
- data/app/views/goodmin/fields/boolean/_index.html.erb +1 -0
- data/app/views/goodmin/fields/boolean/_show.html.erb +1 -0
- data/app/views/goodmin/fields/date/_form.html.erb +6 -0
- data/app/views/goodmin/fields/date/_index.html.erb +1 -0
- data/app/views/goodmin/fields/date/_show.html.erb +1 -0
- data/app/views/goodmin/fields/date_time/_form.html.erb +6 -0
- data/app/views/goodmin/fields/date_time/_index.html.erb +1 -0
- data/app/views/goodmin/fields/date_time/_show.html.erb +1 -0
- data/app/views/goodmin/fields/enum/_index.html.erb +1 -0
- data/app/views/goodmin/fields/enum/_show.html.erb +1 -0
- data/app/views/goodmin/fields/nested_has_one/_form.html.erb +6 -0
- data/app/views/goodmin/fields/nested_has_one/_index.html.erb +1 -0
- data/app/views/goodmin/fields/nested_has_one/_show.html.erb +1 -0
- data/app/views/goodmin/fields/number/_form.html.erb +5 -0
- data/app/views/goodmin/fields/number/_index.html.erb +1 -0
- data/app/views/goodmin/fields/number/_show.html.erb +1 -0
- data/app/views/goodmin/fields/password/_form.html.erb +5 -0
- data/app/views/goodmin/fields/password/_index.html.erb +1 -0
- data/app/views/goodmin/fields/password/_show.html.erb +1 -0
- data/app/views/goodmin/fields/select/_form.html.erb +5 -0
- data/app/views/goodmin/fields/select/_index.html.erb +1 -0
- data/app/views/goodmin/fields/select/_show.html.erb +1 -0
- data/app/views/goodmin/fields/string/_form.html.erb +5 -0
- data/app/views/goodmin/fields/string/_index.html.erb +1 -0
- data/app/views/goodmin/fields/string/_show.html.erb +1 -0
- data/app/views/goodmin/fields/text/_form.html.erb +5 -0
- data/app/views/goodmin/fields/text/_index.html.erb +1 -0
- data/app/views/goodmin/fields/text/_show.html.erb +1 -0
- data/app/views/goodmin/resource/_actions.html.erb +9 -0
- data/app/views/goodmin/resource/_batch_actions.html.erb +12 -0
- data/app/views/goodmin/resource/_breadcrumb.html.erb +33 -0
- data/app/views/goodmin/resource/_breadcrumb_actions.html.erb +41 -0
- data/app/views/goodmin/resource/_button_actions.html.erb +3 -0
- data/app/views/goodmin/resource/_errors.html.erb +9 -0
- data/app/views/goodmin/resource/_export_actions.html.erb +15 -0
- data/app/views/goodmin/resource/_filters.html.erb +22 -0
- data/app/views/goodmin/resource/_form.html.erb +26 -0
- data/app/views/goodmin/resource/_pagination.html.erb +40 -0
- data/app/views/goodmin/resource/_scopes.html.erb +14 -0
- data/app/views/goodmin/resource/_table.html.erb +45 -0
- data/app/views/goodmin/resource/columns/_actions.html.erb +28 -0
- data/app/views/goodmin/resource/edit.html.erb +5 -0
- data/app/views/goodmin/resource/index.csv.csvbuilder +5 -0
- data/app/views/goodmin/resource/index.html.erb +10 -0
- data/app/views/goodmin/resource/index.json.jbuilder +3 -0
- data/app/views/goodmin/resource/new.html.erb +5 -0
- data/app/views/goodmin/resource/show.html.erb +11 -0
- data/app/views/goodmin/resource/show.json.jbuilder +1 -0
- data/app/views/goodmin/sessions/new.html.erb +11 -0
- data/app/views/goodmin/shared/_navigation.html.erb +0 -0
- data/app/views/goodmin/shared/_navigation_aside.html.erb +7 -0
- data/app/views/layouts/goodmin/_content.html.erb +13 -0
- data/app/views/layouts/goodmin/_layout.html.erb +22 -0
- data/app/views/layouts/goodmin/application.html.erb +28 -0
- data/app/views/layouts/goodmin/login.html.erb +18 -0
- data/config/importmap.rb +5 -0
- data/config/locales/en.yml +49 -0
- data/config/locales/pl-BR.yml +49 -0
- data/config/locales/pt-BR.yml +49 -0
- data/config/locales/sv.yml +49 -0
- data/config/routes.rb +3 -0
- data/lib/generators/goodmin/authentication/authentication_generator.rb +41 -0
- data/lib/generators/goodmin/authentication/templates/sessions_controller.rb +9 -0
- data/lib/generators/goodmin/install/install_generator.rb +41 -0
- data/lib/generators/goodmin/policy/policy_generator.rb +7 -0
- data/lib/generators/goodmin/policy/templates/policy.rb +23 -0
- data/lib/generators/goodmin/resource/resource_generator.rb +31 -0
- data/lib/generators/goodmin/resource/templates/resource.rb +25 -0
- data/lib/generators/goodmin/resource/templates/resource_controller.rb +9 -0
- data/lib/generators/goodmin/resource/templates/resource_model.rb +4 -0
- data/lib/generators/goodmin/resource/templates/resource_service.rb +23 -0
- data/lib/goodmin/authentication/sessions_controller.rb +46 -0
- data/lib/goodmin/authentication/user.rb +27 -0
- data/lib/goodmin/authentication.rb +35 -0
- data/lib/goodmin/authorization/policy.rb +41 -0
- data/lib/goodmin/authorization.rb +69 -0
- data/lib/goodmin/engine.rb +30 -0
- data/lib/goodmin/fields/association.rb +62 -0
- data/lib/goodmin/fields/base.rb +57 -0
- data/lib/goodmin/fields/boolean.rb +6 -0
- data/lib/goodmin/fields/date.rb +6 -0
- data/lib/goodmin/fields/date_time.rb +6 -0
- data/lib/goodmin/fields/enum.rb +15 -0
- data/lib/goodmin/fields/nested_has_one.rb +41 -0
- data/lib/goodmin/fields/number.rb +6 -0
- data/lib/goodmin/fields/password.rb +6 -0
- data/lib/goodmin/fields/select.rb +19 -0
- data/lib/goodmin/fields/string.rb +6 -0
- data/lib/goodmin/fields/text.rb +6 -0
- data/lib/goodmin/generators/base.rb +49 -0
- data/lib/goodmin/generators/named_base.rb +31 -0
- data/lib/goodmin/helpers/application.rb +47 -0
- data/lib/goodmin/helpers/batch_actions.rb +21 -0
- data/lib/goodmin/helpers/filters.rb +123 -0
- data/lib/goodmin/helpers/forms.rb +42 -0
- data/lib/goodmin/helpers/navigation.rb +52 -0
- data/lib/goodmin/helpers/tables.rb +25 -0
- data/lib/goodmin/helpers/translations.rb +19 -0
- data/lib/goodmin/paginator.rb +55 -0
- data/lib/goodmin/resolver.rb +141 -0
- data/lib/goodmin/resources/attribute.rb +46 -0
- data/lib/goodmin/resources/form_builder.rb +96 -0
- data/lib/goodmin/resources/form_component.rb +65 -0
- data/lib/goodmin/resources/form_components/col.rb +33 -0
- data/lib/goodmin/resources/form_components/row.rb +25 -0
- data/lib/goodmin/resources/form_components/section.rb +51 -0
- data/lib/goodmin/resources/form_components/tab.rb +49 -0
- data/lib/goodmin/resources/resource/associations.rb +23 -0
- data/lib/goodmin/resources/resource/batch_actions.rb +56 -0
- data/lib/goodmin/resources/resource/filters.rb +44 -0
- data/lib/goodmin/resources/resource/ordering.rb +41 -0
- data/lib/goodmin/resources/resource/pagination.rb +22 -0
- data/lib/goodmin/resources/resource/scopes.rb +61 -0
- data/lib/goodmin/resources/resource.rb +199 -0
- data/lib/goodmin/resources/resource_controller/batch_actions.rb +49 -0
- data/lib/goodmin/resources/resource_service/associations.rb +23 -0
- data/lib/goodmin/resources/resource_service/batch_actions.rb +52 -0
- data/lib/goodmin/resources/resource_service/filters.rb +44 -0
- data/lib/goodmin/resources/resource_service/ordering.rb +41 -0
- data/lib/goodmin/resources/resource_service/pagination.rb +22 -0
- data/lib/goodmin/resources/resource_service/scopes.rb +61 -0
- data/lib/goodmin/resources/resource_service.rb +199 -0
- data/lib/goodmin/service_locator.rb +25 -0
- data/lib/goodmin/version.rb +3 -0
- data/lib/goodmin.rb +44 -0
- data/lib/tasks/goodmin_tasks.rake +4 -0
- metadata +461 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
module Goodmin
|
|
2
|
+
class Paginator
|
|
3
|
+
WINDOW_SIZE = 7
|
|
4
|
+
|
|
5
|
+
attr_reader :per_page, :current_page
|
|
6
|
+
|
|
7
|
+
def initialize(resources, per_page: 25, current_page: nil)
|
|
8
|
+
@resources = resources
|
|
9
|
+
@per_page = per_page
|
|
10
|
+
@current_page = current_page ? current_page.to_i : 1
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def paginate
|
|
14
|
+
@resources.limit(per_page).offset(offset)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def pages
|
|
18
|
+
@pages ||= begin
|
|
19
|
+
pages = (1..total_pages).to_a
|
|
20
|
+
|
|
21
|
+
return pages unless total_pages > WINDOW_SIZE
|
|
22
|
+
|
|
23
|
+
if current_page < WINDOW_SIZE
|
|
24
|
+
pages.slice(0, WINDOW_SIZE)
|
|
25
|
+
elsif current_page > (total_pages - WINDOW_SIZE)
|
|
26
|
+
pages.slice(-WINDOW_SIZE, WINDOW_SIZE)
|
|
27
|
+
else
|
|
28
|
+
pages.slice(pages.index(current_page) - (WINDOW_SIZE / 2), WINDOW_SIZE)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def total_pages
|
|
34
|
+
@total_pages ||= (total_resources.to_f / per_page).ceil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def total_resources
|
|
38
|
+
@total_resources ||= begin
|
|
39
|
+
count = @resources.count
|
|
40
|
+
|
|
41
|
+
if count.respond_to?(:count)
|
|
42
|
+
count.count
|
|
43
|
+
else
|
|
44
|
+
count
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def offset
|
|
52
|
+
(current_page * per_page) - per_page
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
require "action_view"
|
|
2
|
+
require "action_view/template/resolver"
|
|
3
|
+
|
|
4
|
+
module Goodmin
|
|
5
|
+
class Resolver < ::ActionView::FileSystemResolver
|
|
6
|
+
def self.resolvers(controller_path)
|
|
7
|
+
engine_root = engine_root_for(controller_path)
|
|
8
|
+
[
|
|
9
|
+
EngineResolver.new(controller_path, engine_root),
|
|
10
|
+
GoodminResolver.new(controller_path)
|
|
11
|
+
]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.engine_root_for(controller_path)
|
|
15
|
+
parts = controller_path.split("/")
|
|
16
|
+
return Rails.application.root if parts.length < 2
|
|
17
|
+
|
|
18
|
+
namespace_path = parts.first(parts.length - 1).join("/")
|
|
19
|
+
|
|
20
|
+
engine = Rails::Engine.subclasses.find do |e|
|
|
21
|
+
next unless e.railtie_namespace
|
|
22
|
+
e.railtie_namespace.name.underscore.gsub("::", "/") == namespace_path
|
|
23
|
+
rescue NameError, NoMethodError
|
|
24
|
+
nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
engine&.root || Rails.application.root
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def initialize(path, controller_path)
|
|
31
|
+
super(path)
|
|
32
|
+
@controller_path = controller_path
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# This function is for Rails 6 and up since the `find_templates` function
|
|
36
|
+
# is deprecated. It does the same thing, just a little differently. It's
|
|
37
|
+
# not being run by versions previous to Rails 6.
|
|
38
|
+
def _find_all(name, prefix, partial, details, key, locals)
|
|
39
|
+
templates = []
|
|
40
|
+
|
|
41
|
+
template_paths(prefix).each do |template_path|
|
|
42
|
+
break if templates.present?
|
|
43
|
+
|
|
44
|
+
templates = super(name, template_path, partial, details, key, locals)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
templates
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# This is how we find templates in Rails 5 and below.
|
|
51
|
+
def find_templates(name, prefix, *args)
|
|
52
|
+
templates = []
|
|
53
|
+
|
|
54
|
+
template_paths(prefix).each do |path|
|
|
55
|
+
break if templates.present?
|
|
56
|
+
|
|
57
|
+
templates = super(name, path, *args)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
templates
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
class EngineResolver < Resolver
|
|
65
|
+
def initialize(controller_path, engine_root = Rails.application.root)
|
|
66
|
+
super(File.join(engine_root, "app/views"), controller_path)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def template_paths(prefix)
|
|
70
|
+
application_path = application_path_for_engine
|
|
71
|
+
resource_path = resource_path_for_engine(prefix)
|
|
72
|
+
shared_path = shared_path_for_engine(prefix)
|
|
73
|
+
# Always include the base shared path (without any prefix sub-path) before application_path.
|
|
74
|
+
# This ensures app/views/goodmin/shared overrides are found even when Rails invokes the
|
|
75
|
+
# resolver with a non-controller prefix (e.g. "layouts/goodmin" from the layout file),
|
|
76
|
+
# where the prefix-specific shared path won't match the user's shared partial.
|
|
77
|
+
# Using @controller_path as the prefix argument produces a sub_path of "" (no suffix),
|
|
78
|
+
# giving us the plain namespace-scoped shared directory (e.g. "shared" or "admin/shared").
|
|
79
|
+
base_shared_path = shared_path_for_engine(@controller_path)
|
|
80
|
+
|
|
81
|
+
paths = [resource_path, shared_path, base_shared_path, application_path].uniq
|
|
82
|
+
return paths if @controller_path.split("/").length > 1
|
|
83
|
+
|
|
84
|
+
[
|
|
85
|
+
"goodmin/#{resource_path}",
|
|
86
|
+
"goodmin/#{shared_path}",
|
|
87
|
+
"goodmin/#{base_shared_path}",
|
|
88
|
+
"goodmin/#{application_path}"
|
|
89
|
+
].uniq + paths
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def application_path_for_engine
|
|
93
|
+
parts = @controller_path.split("/")
|
|
94
|
+
parts.length > 1 ? "#{parts.first(parts.length - 1).join("/")}/application" : "application"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def resource_path_for_engine(prefix)
|
|
98
|
+
sub_path = prefix.delete_prefix(@controller_path).delete_prefix("/")
|
|
99
|
+
parts = @controller_path.split("/")
|
|
100
|
+
resource_base = parts.length > 1 ? "#{parts.first(parts.length - 1).join("/")}/resource" : "resource"
|
|
101
|
+
sub_path.present? ? "#{resource_base}/#{sub_path}" : resource_base
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def shared_path_for_engine(prefix)
|
|
105
|
+
sub_path = prefix.delete_prefix(@controller_path).delete_prefix("/")
|
|
106
|
+
parts = @controller_path.split("/")
|
|
107
|
+
shared_base = parts.length > 1 ? "#{parts.first(parts.length - 1).join("/")}/shared" : "shared"
|
|
108
|
+
sub_path.present? ? "#{shared_base}/#{sub_path}" : shared_base
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
class GoodminResolver < Resolver
|
|
113
|
+
def initialize(controller_path)
|
|
114
|
+
super(File.join(Goodmin::Engine.root, "app/views/goodmin"), controller_path)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def template_paths(prefix)
|
|
118
|
+
[
|
|
119
|
+
default_path_for_goodmin(prefix),
|
|
120
|
+
resource_path_for_goodmin(prefix),
|
|
121
|
+
shared_path_for_goodmin(prefix)
|
|
122
|
+
]
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def default_path_for_goodmin(prefix)
|
|
126
|
+
sub_path = prefix.delete_prefix(@controller_path).delete_prefix("/")
|
|
127
|
+
base = File.basename(@controller_path)
|
|
128
|
+
sub_path.present? ? "#{base}/#{sub_path}" : base
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def resource_path_for_goodmin(prefix)
|
|
132
|
+
sub_path = prefix.delete_prefix(@controller_path).delete_prefix("/")
|
|
133
|
+
sub_path.present? ? "resource/#{sub_path}" : "resource"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def shared_path_for_goodmin(prefix)
|
|
137
|
+
sub_path = prefix.delete_prefix(@controller_path).delete_prefix("/")
|
|
138
|
+
sub_path.present? ? "shared/#{sub_path}" : "shared"
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module Goodmin
|
|
2
|
+
module Resources
|
|
3
|
+
class Attribute
|
|
4
|
+
attr_reader :name, :field_class, :options
|
|
5
|
+
|
|
6
|
+
def initialize(name, field_class: nil, **options)
|
|
7
|
+
@name = name
|
|
8
|
+
@field_class = field_class
|
|
9
|
+
@options = options
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def to_field(record:, resource_service:)
|
|
13
|
+
resolved_field_class(record).new(
|
|
14
|
+
attribute: name,
|
|
15
|
+
record: record,
|
|
16
|
+
resource_service: resource_service,
|
|
17
|
+
**options
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def resolved_field_class(record)
|
|
24
|
+
return field_class if field_class
|
|
25
|
+
|
|
26
|
+
if record.class.respond_to?(:reflect_on_association) && (reflection = record.class.reflect_on_association(name))
|
|
27
|
+
reflection.macro == :has_one ? Fields::NestedHasOne : Fields::Association
|
|
28
|
+
elsif record.class.respond_to?(:defined_enums) && record.class.defined_enums.key?(name.to_s)
|
|
29
|
+
Fields::Enum
|
|
30
|
+
elsif record.class.respond_to?(:has_attribute?) && record.class.has_attribute?(name.to_s)
|
|
31
|
+
column = record.class.column_for_attribute(name)
|
|
32
|
+
case column.type
|
|
33
|
+
when :text then Fields::Text
|
|
34
|
+
when :boolean then Fields::Boolean
|
|
35
|
+
when :date then Fields::Date
|
|
36
|
+
when :datetime, :timestamp then Fields::DateTime
|
|
37
|
+
when :integer, :float, :decimal then Fields::Number
|
|
38
|
+
else Fields::String
|
|
39
|
+
end
|
|
40
|
+
else
|
|
41
|
+
Fields::String
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
require "goodmin/resources/attribute"
|
|
2
|
+
|
|
3
|
+
module Goodmin
|
|
4
|
+
module Resources
|
|
5
|
+
class FormBuilder
|
|
6
|
+
HTML_TAGS = %w[
|
|
7
|
+
div span p fieldset article header footer main
|
|
8
|
+
h1 h2 h3 h4 h5 h6 ul ol li dl dt dd
|
|
9
|
+
].freeze
|
|
10
|
+
|
|
11
|
+
attr_reader :nodes
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
@nodes = []
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def attribute(name, field: nil, **options)
|
|
18
|
+
@nodes << AttributeNode.new(Attribute.new(name, field_class: field, **options))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
HTML_TAGS.each do |tag|
|
|
22
|
+
define_method(tag) do |**html_attrs, &block|
|
|
23
|
+
child_builder = FormBuilder.new
|
|
24
|
+
child_builder.instance_eval(&block) if block
|
|
25
|
+
@nodes << HtmlNode.new(tag, html_attrs, child_builder.nodes)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Registers a custom DSL component so it can be used inside form blocks.
|
|
30
|
+
#
|
|
31
|
+
# @param name [Symbol] the method name available in the DSL
|
|
32
|
+
# @param klass [Class] a class that includes Goodmin::Resources::FormComponent
|
|
33
|
+
#
|
|
34
|
+
# Example:
|
|
35
|
+
# Goodmin::Resources::FormBuilder.register_component(:my_component, MyComponent)
|
|
36
|
+
def self.register_component(name, klass)
|
|
37
|
+
define_method(name) do |*args, **kwargs, &block|
|
|
38
|
+
child_builder = FormBuilder.new
|
|
39
|
+
child_builder.instance_eval(&block) if block
|
|
40
|
+
@nodes << ComponentNode.new(klass.new(child_builder.nodes, *args, **kwargs))
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Extracts a flat list of Attribute objects from a node tree.
|
|
45
|
+
# Used internally and by FormComponent#attributes.
|
|
46
|
+
def self.extract_attributes(nodes)
|
|
47
|
+
nodes.flat_map do |node|
|
|
48
|
+
case node
|
|
49
|
+
when AttributeNode then [node.attribute]
|
|
50
|
+
when HtmlNode then extract_attributes(node.children)
|
|
51
|
+
when ComponentNode then node.component.attributes
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Extracts the list of Tab components from a top-level node array.
|
|
57
|
+
def self.extract_tabs(nodes)
|
|
58
|
+
nodes.select { |n| n.is_a?(ComponentNode) && n.component.tab_component? }.map(&:component)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def attributes
|
|
62
|
+
self.class.extract_attributes(@nodes)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def tabs
|
|
66
|
+
self.class.extract_tabs(@nodes)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
class AttributeNode
|
|
71
|
+
attr_reader :attribute
|
|
72
|
+
|
|
73
|
+
def initialize(attribute)
|
|
74
|
+
@attribute = attribute
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
class HtmlNode
|
|
79
|
+
attr_reader :tag, :html_attrs, :children
|
|
80
|
+
|
|
81
|
+
def initialize(tag, html_attrs, children)
|
|
82
|
+
@tag = tag
|
|
83
|
+
@html_attrs = html_attrs
|
|
84
|
+
@children = children
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
class ComponentNode
|
|
89
|
+
attr_reader :component
|
|
90
|
+
|
|
91
|
+
def initialize(component)
|
|
92
|
+
@component = component
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
module Goodmin
|
|
2
|
+
module Resources
|
|
3
|
+
# Mixin for custom form DSL components that can be registered with
|
|
4
|
+
# FormBuilder.register_component and used inside form blocks.
|
|
5
|
+
#
|
|
6
|
+
# To create a custom component, include this module and implement:
|
|
7
|
+
#
|
|
8
|
+
# - #initialize(children, *args, **kwargs) — receives the list of child
|
|
9
|
+
# nodes (Array of AttributeNode / HtmlNode / ComponentNode) as the
|
|
10
|
+
# first argument, followed by any arguments passed in the DSL.
|
|
11
|
+
# - #render(view_context, f) — returns HTML-safe output for the component.
|
|
12
|
+
# - (optional) #attributes — returns an Array of Goodmin::Resources::Attribute
|
|
13
|
+
# objects contributed by this component for strong-parameter filtering.
|
|
14
|
+
# Defaults to extracting attributes from any child nodes.
|
|
15
|
+
#
|
|
16
|
+
# Example:
|
|
17
|
+
#
|
|
18
|
+
# class MyComponent
|
|
19
|
+
# include Goodmin::Resources::FormComponent
|
|
20
|
+
#
|
|
21
|
+
# def initialize(children, label:)
|
|
22
|
+
# super(children)
|
|
23
|
+
# @label = label
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
# def render(view_context, f)
|
|
27
|
+
# view_context.content_tag(:fieldset) do
|
|
28
|
+
# view_context.content_tag(:legend, @label) +
|
|
29
|
+
# view_context.render_form_nodes(children, f)
|
|
30
|
+
# end
|
|
31
|
+
# end
|
|
32
|
+
# end
|
|
33
|
+
#
|
|
34
|
+
# # Register:
|
|
35
|
+
# Goodmin::Resources::FormBuilder.register_component(:my_component, MyComponent)
|
|
36
|
+
#
|
|
37
|
+
# # Use in a form block:
|
|
38
|
+
# form do
|
|
39
|
+
# my_component(label: "Details") do
|
|
40
|
+
# attribute :title
|
|
41
|
+
# end
|
|
42
|
+
# end
|
|
43
|
+
module FormComponent
|
|
44
|
+
def self.included(base)
|
|
45
|
+
base.attr_reader :children
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def initialize(children, *_args, **_kwargs)
|
|
49
|
+
@children = children
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Override to return Attribute objects this component contributes.
|
|
53
|
+
# By default, attributes are extracted recursively from child nodes.
|
|
54
|
+
def attributes
|
|
55
|
+
FormBuilder.extract_attributes(children)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Returns true for tab components. Override in tab-like components to
|
|
59
|
+
# allow FormBuilder to identify and extract them.
|
|
60
|
+
def tab_component?
|
|
61
|
+
false
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module Goodmin
|
|
2
|
+
module Resources
|
|
3
|
+
module FormComponents
|
|
4
|
+
# Renders a Bootstrap grid column wrapping its child form nodes.
|
|
5
|
+
#
|
|
6
|
+
# Accepts an optional +size+ keyword argument (default: 12) which maps to
|
|
7
|
+
# the Bootstrap col-md-* class.
|
|
8
|
+
#
|
|
9
|
+
# Usage in a form block:
|
|
10
|
+
#
|
|
11
|
+
# form do
|
|
12
|
+
# row do
|
|
13
|
+
# col(size: 6) { attribute :title }
|
|
14
|
+
# col(size: 6) { attribute :body }
|
|
15
|
+
# end
|
|
16
|
+
# end
|
|
17
|
+
class Col
|
|
18
|
+
include FormComponent
|
|
19
|
+
|
|
20
|
+
def initialize(children, size: 12)
|
|
21
|
+
super(children)
|
|
22
|
+
@size = size
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def render(view_context, f)
|
|
26
|
+
view_context.content_tag(:div, class: "col-md-#{@size}") do
|
|
27
|
+
view_context.render_form_nodes(children, f)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Goodmin
|
|
2
|
+
module Resources
|
|
3
|
+
module FormComponents
|
|
4
|
+
# Renders a Bootstrap grid row wrapping its child form nodes.
|
|
5
|
+
#
|
|
6
|
+
# Usage in a form block:
|
|
7
|
+
#
|
|
8
|
+
# form do
|
|
9
|
+
# row do
|
|
10
|
+
# col { attribute :title }
|
|
11
|
+
# col { attribute :body }
|
|
12
|
+
# end
|
|
13
|
+
# end
|
|
14
|
+
class Row
|
|
15
|
+
include FormComponent
|
|
16
|
+
|
|
17
|
+
def render(view_context, f)
|
|
18
|
+
view_context.content_tag(:div, class: "row") do
|
|
19
|
+
view_context.render_form_nodes(children, f)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module Goodmin
|
|
2
|
+
module Resources
|
|
3
|
+
module FormComponents
|
|
4
|
+
# Renders a titled, described section wrapping its child form nodes.
|
|
5
|
+
#
|
|
6
|
+
# Accepts optional +title+ and +description+ keyword arguments which are
|
|
7
|
+
# rendered above a +<div>+ that wraps the section's child nodes. The entire
|
|
8
|
+
# output is wrapped in an outer +<div>+.
|
|
9
|
+
#
|
|
10
|
+
# Both +title+ and +description+ can be a static string or a +Proc+. When
|
|
11
|
+
# a +Proc+ is provided it is called at render time with the form's object
|
|
12
|
+
# (the resource record) as its sole argument, allowing dynamic content.
|
|
13
|
+
#
|
|
14
|
+
# Usage in a form block:
|
|
15
|
+
#
|
|
16
|
+
# form do
|
|
17
|
+
# section(title: "Details", description: "Fill in the details below.") do
|
|
18
|
+
# attribute :title
|
|
19
|
+
# attribute :body
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# section(description: ->(record) { "Editing #{record.name}" }) do
|
|
23
|
+
# attribute :title
|
|
24
|
+
# end
|
|
25
|
+
# end
|
|
26
|
+
class Section
|
|
27
|
+
include FormComponent
|
|
28
|
+
|
|
29
|
+
def initialize(children, title: nil, description: nil)
|
|
30
|
+
super(children)
|
|
31
|
+
@title = title
|
|
32
|
+
@description = description
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def render(view_context, f)
|
|
36
|
+
record = f.object
|
|
37
|
+
title = @title.is_a?(Proc) ? @title.call(record) : @title
|
|
38
|
+
description = @description.is_a?(Proc) ? @description.call(record) : @description
|
|
39
|
+
|
|
40
|
+
view_context.content_tag(:div) do
|
|
41
|
+
parts = []
|
|
42
|
+
parts << view_context.content_tag(:h4, title) if title.present?
|
|
43
|
+
parts << view_context.content_tag(:p, description) if description.present?
|
|
44
|
+
parts << view_context.content_tag(:div) { view_context.render_form_nodes(children, f) }
|
|
45
|
+
view_context.safe_join(parts)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module Goodmin
|
|
2
|
+
module Resources
|
|
3
|
+
module FormComponents
|
|
4
|
+
# Renders a form tab panel containing its child form nodes.
|
|
5
|
+
#
|
|
6
|
+
# Each tab has a +title+ and a derived +key+ (parameterized from the title)
|
|
7
|
+
# used as the URL query parameter value to identify the active tab.
|
|
8
|
+
#
|
|
9
|
+
# When the resource form has tabs defined, only one tab's content is
|
|
10
|
+
# rendered at a time based on the +?tab=<key>+ query parameter. A sidebar
|
|
11
|
+
# with links to each tab is displayed alongside the form.
|
|
12
|
+
#
|
|
13
|
+
# Usage in a form block:
|
|
14
|
+
#
|
|
15
|
+
# form do
|
|
16
|
+
# tab title: "General settings" do
|
|
17
|
+
# attribute :name
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# tab title: "Config" do
|
|
21
|
+
# row do
|
|
22
|
+
# col { attribute :foo }
|
|
23
|
+
# end
|
|
24
|
+
# end
|
|
25
|
+
# end
|
|
26
|
+
class Tab
|
|
27
|
+
include FormComponent
|
|
28
|
+
|
|
29
|
+
attr_reader :title, :key
|
|
30
|
+
|
|
31
|
+
def initialize(children, title:)
|
|
32
|
+
raise ArgumentError, "Tab requires a :title" if title.blank?
|
|
33
|
+
|
|
34
|
+
super(children)
|
|
35
|
+
@title = title
|
|
36
|
+
@key = title.parameterize(separator: "_")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def render(view_context, f)
|
|
40
|
+
view_context.render_form_nodes(children, f)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def tab_component?
|
|
44
|
+
true
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Goodmin
|
|
2
|
+
module Resources
|
|
3
|
+
module Resource
|
|
4
|
+
module Associations
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
delegate :has_many_map, to: "self.class"
|
|
8
|
+
|
|
9
|
+
module ClassMethods
|
|
10
|
+
def has_many_map
|
|
11
|
+
@has_many_map ||= {}
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def has_many(attr, options = {})
|
|
15
|
+
has_many_map[attr] = {
|
|
16
|
+
class_name: attr.to_s.singularize.classify
|
|
17
|
+
}.merge(options)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
module Goodmin
|
|
2
|
+
module Resources
|
|
3
|
+
module Resource
|
|
4
|
+
module BatchActions
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
delegate :batch_action_map, to: "self.class"
|
|
8
|
+
|
|
9
|
+
def batch_action(action, records)
|
|
10
|
+
if batch_action?(action)
|
|
11
|
+
send("batch_action_#{action}", records)
|
|
12
|
+
true
|
|
13
|
+
else
|
|
14
|
+
false
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def batch_action?(action)
|
|
19
|
+
batch_action_map.key?(action.to_sym)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def include_batch_action?(action)
|
|
23
|
+
return false unless batch_action_map.key?(action.to_sym)
|
|
24
|
+
|
|
25
|
+
options = batch_action_map[action.to_sym]
|
|
26
|
+
|
|
27
|
+
if options[:only].present?
|
|
28
|
+
options[:only].include?(@scope.to_sym)
|
|
29
|
+
elsif options[:except].present?
|
|
30
|
+
!options[:except].include?(@scope.to_sym)
|
|
31
|
+
else
|
|
32
|
+
true
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def include_batch_actions?
|
|
37
|
+
batch_action_map.any? { |action, _options| include_batch_action?(action) }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
module ClassMethods
|
|
41
|
+
def batch_action_map
|
|
42
|
+
@batch_action_map ||= {}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def batch_action(attr, options = {})
|
|
46
|
+
batch_action_map[attr] = {
|
|
47
|
+
confirm: false,
|
|
48
|
+
only: nil,
|
|
49
|
+
except: nil
|
|
50
|
+
}.merge(options)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module Goodmin
|
|
2
|
+
module Resources
|
|
3
|
+
module Resource
|
|
4
|
+
module Filters
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
delegate :filter_map, to: "self.class"
|
|
8
|
+
|
|
9
|
+
def apply_filters(filter_params, resources)
|
|
10
|
+
if filter_params.present?
|
|
11
|
+
filter_params.each do |name, value|
|
|
12
|
+
if apply_filter?(name, value)
|
|
13
|
+
resources = send("filter_#{name}", resources, value)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
resources
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def apply_filter?(name, value)
|
|
23
|
+
return false if value == [""]
|
|
24
|
+
filter_map.key?(name.to_sym) && value.present?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
module ClassMethods
|
|
28
|
+
def filter_map
|
|
29
|
+
@filter_map ||= {}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def filter(attr, options = {})
|
|
33
|
+
filter_map[attr] = {
|
|
34
|
+
as: :string,
|
|
35
|
+
option_text: "to_s",
|
|
36
|
+
option_value: "id",
|
|
37
|
+
collection: nil
|
|
38
|
+
}.merge(options)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|