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.
Files changed (147) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +1359 -0
  4. data/Rakefile +56 -0
  5. data/app/assets/stylesheets/goodmin/application.css +75 -0
  6. data/app/controllers/goodmin/application_controller.rb +43 -0
  7. data/app/controllers/goodmin/resource_controller.rb +235 -0
  8. data/app/helpers/goodmin/application_helper.rb +45 -0
  9. data/app/javascript/goodmin/application.js +8 -0
  10. data/app/javascript/goodmin/controllers/batch_actions_controller.js +101 -0
  11. data/app/javascript/goodmin/controllers/datetimepicker_controller.js +24 -0
  12. data/app/javascript/goodmin/controllers/navigation_controller.js +30 -0
  13. data/app/jobs/goodmin/application_job.rb +4 -0
  14. data/app/mailers/goodmin/application_mailer.rb +6 -0
  15. data/app/models/goodmin/application_record.rb +5 -0
  16. data/app/views/goodmin/application/welcome.html.erb +17 -0
  17. data/app/views/goodmin/fields/association/_form.html.erb +19 -0
  18. data/app/views/goodmin/fields/association/_index.html.erb +6 -0
  19. data/app/views/goodmin/fields/association/_show.html.erb +6 -0
  20. data/app/views/goodmin/fields/boolean/_form.html.erb +7 -0
  21. data/app/views/goodmin/fields/boolean/_index.html.erb +1 -0
  22. data/app/views/goodmin/fields/boolean/_show.html.erb +1 -0
  23. data/app/views/goodmin/fields/date/_form.html.erb +6 -0
  24. data/app/views/goodmin/fields/date/_index.html.erb +1 -0
  25. data/app/views/goodmin/fields/date/_show.html.erb +1 -0
  26. data/app/views/goodmin/fields/date_time/_form.html.erb +6 -0
  27. data/app/views/goodmin/fields/date_time/_index.html.erb +1 -0
  28. data/app/views/goodmin/fields/date_time/_show.html.erb +1 -0
  29. data/app/views/goodmin/fields/enum/_index.html.erb +1 -0
  30. data/app/views/goodmin/fields/enum/_show.html.erb +1 -0
  31. data/app/views/goodmin/fields/nested_has_one/_form.html.erb +6 -0
  32. data/app/views/goodmin/fields/nested_has_one/_index.html.erb +1 -0
  33. data/app/views/goodmin/fields/nested_has_one/_show.html.erb +1 -0
  34. data/app/views/goodmin/fields/number/_form.html.erb +5 -0
  35. data/app/views/goodmin/fields/number/_index.html.erb +1 -0
  36. data/app/views/goodmin/fields/number/_show.html.erb +1 -0
  37. data/app/views/goodmin/fields/password/_form.html.erb +5 -0
  38. data/app/views/goodmin/fields/password/_index.html.erb +1 -0
  39. data/app/views/goodmin/fields/password/_show.html.erb +1 -0
  40. data/app/views/goodmin/fields/select/_form.html.erb +5 -0
  41. data/app/views/goodmin/fields/select/_index.html.erb +1 -0
  42. data/app/views/goodmin/fields/select/_show.html.erb +1 -0
  43. data/app/views/goodmin/fields/string/_form.html.erb +5 -0
  44. data/app/views/goodmin/fields/string/_index.html.erb +1 -0
  45. data/app/views/goodmin/fields/string/_show.html.erb +1 -0
  46. data/app/views/goodmin/fields/text/_form.html.erb +5 -0
  47. data/app/views/goodmin/fields/text/_index.html.erb +1 -0
  48. data/app/views/goodmin/fields/text/_show.html.erb +1 -0
  49. data/app/views/goodmin/resource/_actions.html.erb +9 -0
  50. data/app/views/goodmin/resource/_batch_actions.html.erb +12 -0
  51. data/app/views/goodmin/resource/_breadcrumb.html.erb +33 -0
  52. data/app/views/goodmin/resource/_breadcrumb_actions.html.erb +41 -0
  53. data/app/views/goodmin/resource/_button_actions.html.erb +3 -0
  54. data/app/views/goodmin/resource/_errors.html.erb +9 -0
  55. data/app/views/goodmin/resource/_export_actions.html.erb +15 -0
  56. data/app/views/goodmin/resource/_filters.html.erb +22 -0
  57. data/app/views/goodmin/resource/_form.html.erb +26 -0
  58. data/app/views/goodmin/resource/_pagination.html.erb +40 -0
  59. data/app/views/goodmin/resource/_scopes.html.erb +14 -0
  60. data/app/views/goodmin/resource/_table.html.erb +45 -0
  61. data/app/views/goodmin/resource/columns/_actions.html.erb +28 -0
  62. data/app/views/goodmin/resource/edit.html.erb +5 -0
  63. data/app/views/goodmin/resource/index.csv.csvbuilder +5 -0
  64. data/app/views/goodmin/resource/index.html.erb +10 -0
  65. data/app/views/goodmin/resource/index.json.jbuilder +3 -0
  66. data/app/views/goodmin/resource/new.html.erb +5 -0
  67. data/app/views/goodmin/resource/show.html.erb +11 -0
  68. data/app/views/goodmin/resource/show.json.jbuilder +1 -0
  69. data/app/views/goodmin/sessions/new.html.erb +11 -0
  70. data/app/views/goodmin/shared/_navigation.html.erb +0 -0
  71. data/app/views/goodmin/shared/_navigation_aside.html.erb +7 -0
  72. data/app/views/layouts/goodmin/_content.html.erb +13 -0
  73. data/app/views/layouts/goodmin/_layout.html.erb +22 -0
  74. data/app/views/layouts/goodmin/application.html.erb +28 -0
  75. data/app/views/layouts/goodmin/login.html.erb +18 -0
  76. data/config/importmap.rb +5 -0
  77. data/config/locales/en.yml +49 -0
  78. data/config/locales/pl-BR.yml +49 -0
  79. data/config/locales/pt-BR.yml +49 -0
  80. data/config/locales/sv.yml +49 -0
  81. data/config/routes.rb +3 -0
  82. data/lib/generators/goodmin/authentication/authentication_generator.rb +41 -0
  83. data/lib/generators/goodmin/authentication/templates/sessions_controller.rb +9 -0
  84. data/lib/generators/goodmin/install/install_generator.rb +41 -0
  85. data/lib/generators/goodmin/policy/policy_generator.rb +7 -0
  86. data/lib/generators/goodmin/policy/templates/policy.rb +23 -0
  87. data/lib/generators/goodmin/resource/resource_generator.rb +31 -0
  88. data/lib/generators/goodmin/resource/templates/resource.rb +25 -0
  89. data/lib/generators/goodmin/resource/templates/resource_controller.rb +9 -0
  90. data/lib/generators/goodmin/resource/templates/resource_model.rb +4 -0
  91. data/lib/generators/goodmin/resource/templates/resource_service.rb +23 -0
  92. data/lib/goodmin/authentication/sessions_controller.rb +46 -0
  93. data/lib/goodmin/authentication/user.rb +27 -0
  94. data/lib/goodmin/authentication.rb +35 -0
  95. data/lib/goodmin/authorization/policy.rb +41 -0
  96. data/lib/goodmin/authorization.rb +69 -0
  97. data/lib/goodmin/engine.rb +30 -0
  98. data/lib/goodmin/fields/association.rb +62 -0
  99. data/lib/goodmin/fields/base.rb +57 -0
  100. data/lib/goodmin/fields/boolean.rb +6 -0
  101. data/lib/goodmin/fields/date.rb +6 -0
  102. data/lib/goodmin/fields/date_time.rb +6 -0
  103. data/lib/goodmin/fields/enum.rb +15 -0
  104. data/lib/goodmin/fields/nested_has_one.rb +41 -0
  105. data/lib/goodmin/fields/number.rb +6 -0
  106. data/lib/goodmin/fields/password.rb +6 -0
  107. data/lib/goodmin/fields/select.rb +19 -0
  108. data/lib/goodmin/fields/string.rb +6 -0
  109. data/lib/goodmin/fields/text.rb +6 -0
  110. data/lib/goodmin/generators/base.rb +49 -0
  111. data/lib/goodmin/generators/named_base.rb +31 -0
  112. data/lib/goodmin/helpers/application.rb +47 -0
  113. data/lib/goodmin/helpers/batch_actions.rb +21 -0
  114. data/lib/goodmin/helpers/filters.rb +123 -0
  115. data/lib/goodmin/helpers/forms.rb +42 -0
  116. data/lib/goodmin/helpers/navigation.rb +52 -0
  117. data/lib/goodmin/helpers/tables.rb +25 -0
  118. data/lib/goodmin/helpers/translations.rb +19 -0
  119. data/lib/goodmin/paginator.rb +55 -0
  120. data/lib/goodmin/resolver.rb +141 -0
  121. data/lib/goodmin/resources/attribute.rb +46 -0
  122. data/lib/goodmin/resources/form_builder.rb +96 -0
  123. data/lib/goodmin/resources/form_component.rb +65 -0
  124. data/lib/goodmin/resources/form_components/col.rb +33 -0
  125. data/lib/goodmin/resources/form_components/row.rb +25 -0
  126. data/lib/goodmin/resources/form_components/section.rb +51 -0
  127. data/lib/goodmin/resources/form_components/tab.rb +49 -0
  128. data/lib/goodmin/resources/resource/associations.rb +23 -0
  129. data/lib/goodmin/resources/resource/batch_actions.rb +56 -0
  130. data/lib/goodmin/resources/resource/filters.rb +44 -0
  131. data/lib/goodmin/resources/resource/ordering.rb +41 -0
  132. data/lib/goodmin/resources/resource/pagination.rb +22 -0
  133. data/lib/goodmin/resources/resource/scopes.rb +61 -0
  134. data/lib/goodmin/resources/resource.rb +199 -0
  135. data/lib/goodmin/resources/resource_controller/batch_actions.rb +49 -0
  136. data/lib/goodmin/resources/resource_service/associations.rb +23 -0
  137. data/lib/goodmin/resources/resource_service/batch_actions.rb +52 -0
  138. data/lib/goodmin/resources/resource_service/filters.rb +44 -0
  139. data/lib/goodmin/resources/resource_service/ordering.rb +41 -0
  140. data/lib/goodmin/resources/resource_service/pagination.rb +22 -0
  141. data/lib/goodmin/resources/resource_service/scopes.rb +61 -0
  142. data/lib/goodmin/resources/resource_service.rb +199 -0
  143. data/lib/goodmin/service_locator.rb +25 -0
  144. data/lib/goodmin/version.rb +3 -0
  145. data/lib/goodmin.rb +44 -0
  146. data/lib/tasks/goodmin_tasks.rake +4 -0
  147. 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