ruby_ui_admin 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +165 -0
  4. data/app/assets/stylesheets/ruby_ui_admin/application.tailwind.css +111 -0
  5. data/app/components/ruby_ui_admin/ui/base.rb +26 -0
  6. data/app/components/ruby_ui_admin/ui/icon.rb +79 -0
  7. data/app/components/ruby_ui_admin/ui/native_select.rb +37 -0
  8. data/app/components/ruby_ui_admin/ui/native_select_group.rb +18 -0
  9. data/app/components/ruby_ui_admin/ui/native_select_icon.rb +42 -0
  10. data/app/components/ruby_ui_admin/ui/native_select_option.rb +18 -0
  11. data/app/components/ruby_ui_admin/ui/pagination.rb +62 -0
  12. data/app/components/ruby_ui_admin/ui/pagination_content.rb +18 -0
  13. data/app/components/ruby_ui_admin/ui/pagination_ellipsis.rb +45 -0
  14. data/app/components/ruby_ui_admin/ui/pagination_item.rb +32 -0
  15. data/app/components/ruby_ui_admin/ui/select.rb +29 -0
  16. data/app/components/ruby_ui_admin/ui/toast_action.rb +30 -0
  17. data/app/components/ruby_ui_admin/ui/toast_cancel.rb +30 -0
  18. data/app/components/ruby_ui_admin/ui/toast_close.rb +43 -0
  19. data/app/components/ruby_ui_admin/ui/toast_description.rb +21 -0
  20. data/app/components/ruby_ui_admin/ui/toast_icon.rb +69 -0
  21. data/app/components/ruby_ui_admin/ui/toast_item.rb +75 -0
  22. data/app/components/ruby_ui_admin/ui/toast_region.rb +147 -0
  23. data/app/components/ruby_ui_admin/ui/toast_title.rb +21 -0
  24. data/app/components/ruby_ui_admin/ui.rb +12 -0
  25. data/app/components/ruby_ui_admin/views/action.rb +37 -0
  26. data/app/components/ruby_ui_admin/views/action_form.rb +104 -0
  27. data/app/components/ruby_ui_admin/views/action_trigger.rb +148 -0
  28. data/app/components/ruby_ui_admin/views/actions_menu.rb +34 -0
  29. data/app/components/ruby_ui_admin/views/base.rb +301 -0
  30. data/app/components/ruby_ui_admin/views/card_component.rb +69 -0
  31. data/app/components/ruby_ui_admin/views/dashboard.rb +40 -0
  32. data/app/components/ruby_ui_admin/views/docs.rb +91 -0
  33. data/app/components/ruby_ui_admin/views/field_input.rb +156 -0
  34. data/app/components/ruby_ui_admin/views/field_value.rb +301 -0
  35. data/app/components/ruby_ui_admin/views/filter_bar.rb +111 -0
  36. data/app/components/ruby_ui_admin/views/form.rb +151 -0
  37. data/app/components/ruby_ui_admin/views/home.rb +35 -0
  38. data/app/components/ruby_ui_admin/views/index.rb +324 -0
  39. data/app/components/ruby_ui_admin/views/path_helpers.rb +36 -0
  40. data/app/components/ruby_ui_admin/views/rails_helpers.rb +15 -0
  41. data/app/components/ruby_ui_admin/views/show.rb +77 -0
  42. data/app/components/ruby_ui_admin/views/show_fields.rb +37 -0
  43. data/app/components/ruby_ui_admin/views/show_tab.rb +58 -0
  44. data/app/components/ruby_ui_admin/views/structure_renderer.rb +148 -0
  45. data/app/components/ruby_ui_admin/views/translation.rb +12 -0
  46. data/app/controllers/ruby_ui_admin/actions_controller.rb +162 -0
  47. data/app/controllers/ruby_ui_admin/application_controller.rb +81 -0
  48. data/app/controllers/ruby_ui_admin/dashboards_controller.rb +12 -0
  49. data/app/controllers/ruby_ui_admin/docs_controller.rb +166 -0
  50. data/app/controllers/ruby_ui_admin/home_controller.rb +18 -0
  51. data/app/controllers/ruby_ui_admin/resources_controller.rb +259 -0
  52. data/config/locales/ruby_ui_admin.en.yml +59 -0
  53. data/config/locales/ruby_ui_admin.pt-BR.yml +59 -0
  54. data/config/routes/dynamic_routes.rb +11 -0
  55. data/config/routes.rb +23 -0
  56. data/docs/README.md +53 -0
  57. data/docs/actions/overview.md +112 -0
  58. data/docs/authorization/action-policy.md +72 -0
  59. data/docs/authorization/adapters.md +101 -0
  60. data/docs/authorization/explicit-mode.md +45 -0
  61. data/docs/authorization/field-authorization.md +53 -0
  62. data/docs/authorization/policies.md +62 -0
  63. data/docs/authorization/scopes.md +48 -0
  64. data/docs/customization/controllers.md +62 -0
  65. data/docs/customization/ejecting.md +55 -0
  66. data/docs/customization/javascript.md +161 -0
  67. data/docs/customization/theming-rubyui.md +80 -0
  68. data/docs/dashboards/overview.md +65 -0
  69. data/docs/fields/overview.md +94 -0
  70. data/docs/filters/overview.md +73 -0
  71. data/docs/generators-and-tasks/overview.md +42 -0
  72. data/docs/getting-started/authentication.md +98 -0
  73. data/docs/getting-started/configuration.md +174 -0
  74. data/docs/getting-started/installation.md +201 -0
  75. data/docs/getting-started/internationalization.md +50 -0
  76. data/docs/getting-started/practical-guide.md +310 -0
  77. data/docs/resources/associations.md +66 -0
  78. data/docs/resources/index-customization.md +68 -0
  79. data/docs/resources/overview.md +50 -0
  80. data/docs/resources/scopes.md +54 -0
  81. data/docs/resources/tabs-panels.md +65 -0
  82. data/lib/generators/ruby_ui_admin/action/action_generator.rb +21 -0
  83. data/lib/generators/ruby_ui_admin/action/templates/action.rb.tt +24 -0
  84. data/lib/generators/ruby_ui_admin/assets/assets_generator.rb +71 -0
  85. data/lib/generators/ruby_ui_admin/card/card_generator.rb +28 -0
  86. data/lib/generators/ruby_ui_admin/card/templates/chart_card.rb.tt +14 -0
  87. data/lib/generators/ruby_ui_admin/card/templates/metric_card.rb.tt +14 -0
  88. data/lib/generators/ruby_ui_admin/card/templates/partial_card.rb.tt +13 -0
  89. data/lib/generators/ruby_ui_admin/components/components_generator.rb +43 -0
  90. data/lib/generators/ruby_ui_admin/controller/controller_generator.rb +30 -0
  91. data/lib/generators/ruby_ui_admin/controller/templates/controller.rb.tt +24 -0
  92. data/lib/generators/ruby_ui_admin/dashboard/dashboard_generator.rb +18 -0
  93. data/lib/generators/ruby_ui_admin/dashboard/templates/dashboard.rb.tt +14 -0
  94. data/lib/generators/ruby_ui_admin/eject/eject_generator.rb +55 -0
  95. data/lib/generators/ruby_ui_admin/filter/filter_generator.rb +28 -0
  96. data/lib/generators/ruby_ui_admin/filter/templates/boolean_filter.rb.tt +13 -0
  97. data/lib/generators/ruby_ui_admin/filter/templates/select_filter.rb.tt +19 -0
  98. data/lib/generators/ruby_ui_admin/filter/templates/text_filter.rb.tt +13 -0
  99. data/lib/generators/ruby_ui_admin/install/install_generator.rb +62 -0
  100. data/lib/generators/ruby_ui_admin/install/templates/initializer.rb.tt +41 -0
  101. data/lib/generators/ruby_ui_admin/locales/locales_generator.rb +22 -0
  102. data/lib/generators/ruby_ui_admin/policy/policy_generator.rb +18 -0
  103. data/lib/generators/ruby_ui_admin/policy/templates/policy.rb.tt +24 -0
  104. data/lib/generators/ruby_ui_admin/resource/resource_generator.rb +84 -0
  105. data/lib/generators/ruby_ui_admin/resource/templates/resource.rb.tt +15 -0
  106. data/lib/generators/ruby_ui_admin/scope/scope_generator.rb +18 -0
  107. data/lib/generators/ruby_ui_admin/scope/templates/scope.rb.tt +11 -0
  108. data/lib/ruby_ui_admin/authorization/action_policy_adapter.rb +69 -0
  109. data/lib/ruby_ui_admin/authorization/adapter.rb +80 -0
  110. data/lib/ruby_ui_admin/authorization/can_can_can_adapter.rb +70 -0
  111. data/lib/ruby_ui_admin/authorization/pundit_adapter.rb +86 -0
  112. data/lib/ruby_ui_admin/authorization.rb +40 -0
  113. data/lib/ruby_ui_admin/base_action.rb +211 -0
  114. data/lib/ruby_ui_admin/base_dashboard.rb +70 -0
  115. data/lib/ruby_ui_admin/base_policy.rb +38 -0
  116. data/lib/ruby_ui_admin/base_resource.rb +503 -0
  117. data/lib/ruby_ui_admin/cards/base_card.rb +59 -0
  118. data/lib/ruby_ui_admin/cards/chart_card.rb +16 -0
  119. data/lib/ruby_ui_admin/cards/metric_card.rb +22 -0
  120. data/lib/ruby_ui_admin/cards/partial_card.rb +14 -0
  121. data/lib/ruby_ui_admin/configuration.rb +177 -0
  122. data/lib/ruby_ui_admin/current.rb +20 -0
  123. data/lib/ruby_ui_admin/dashboard_manager.rb +52 -0
  124. data/lib/ruby_ui_admin/engine.rb +80 -0
  125. data/lib/ruby_ui_admin/execution_context.rb +36 -0
  126. data/lib/ruby_ui_admin/fields/association_field.rb +67 -0
  127. data/lib/ruby_ui_admin/fields/badge_field.rb +19 -0
  128. data/lib/ruby_ui_admin/fields/base_field.rb +208 -0
  129. data/lib/ruby_ui_admin/fields/belongs_to_field.rb +49 -0
  130. data/lib/ruby_ui_admin/fields/boolean_field.rb +13 -0
  131. data/lib/ruby_ui_admin/fields/boolean_group_field.rb +54 -0
  132. data/lib/ruby_ui_admin/fields/code_field.rb +18 -0
  133. data/lib/ruby_ui_admin/fields/date_field.rb +34 -0
  134. data/lib/ruby_ui_admin/fields/date_time_field.rb +37 -0
  135. data/lib/ruby_ui_admin/fields/field_manager.rb +27 -0
  136. data/lib/ruby_ui_admin/fields/file_field.rb +67 -0
  137. data/lib/ruby_ui_admin/fields/files_field.rb +62 -0
  138. data/lib/ruby_ui_admin/fields/has_and_belongs_to_many_field.rb +9 -0
  139. data/lib/ruby_ui_admin/fields/has_many_field.rb +24 -0
  140. data/lib/ruby_ui_admin/fields/has_one_field.rb +13 -0
  141. data/lib/ruby_ui_admin/fields/hidden_field.rb +14 -0
  142. data/lib/ruby_ui_admin/fields/id_field.rb +19 -0
  143. data/lib/ruby_ui_admin/fields/key_value_field.rb +41 -0
  144. data/lib/ruby_ui_admin/fields/number_field.rb +13 -0
  145. data/lib/ruby_ui_admin/fields/password_field.rb +21 -0
  146. data/lib/ruby_ui_admin/fields/record_link_field.rb +45 -0
  147. data/lib/ruby_ui_admin/fields/select_field.rb +71 -0
  148. data/lib/ruby_ui_admin/fields/status_field.rb +21 -0
  149. data/lib/ruby_ui_admin/fields/text_field.rb +14 -0
  150. data/lib/ruby_ui_admin/fields/textarea_field.rb +13 -0
  151. data/lib/ruby_ui_admin/fields/url_field.rb +21 -0
  152. data/lib/ruby_ui_admin/filters/base_filter.rb +81 -0
  153. data/lib/ruby_ui_admin/filters/boolean_filter.rb +9 -0
  154. data/lib/ruby_ui_admin/filters/multiple_select_filter.rb +11 -0
  155. data/lib/ruby_ui_admin/filters/select_filter.rb +9 -0
  156. data/lib/ruby_ui_admin/filters/text_filter.rb +9 -0
  157. data/lib/ruby_ui_admin/menu/builder.rb +94 -0
  158. data/lib/ruby_ui_admin/resource_manager.rb +82 -0
  159. data/lib/ruby_ui_admin/scopes/base_scope.rb +107 -0
  160. data/lib/ruby_ui_admin/services/authorization_service.rb +38 -0
  161. data/lib/ruby_ui_admin/structure.rb +40 -0
  162. data/lib/ruby_ui_admin/tailwind_source.rb +65 -0
  163. data/lib/ruby_ui_admin/version.rb +5 -0
  164. data/lib/ruby_ui_admin/view.rb +34 -0
  165. data/lib/ruby_ui_admin.rb +110 -0
  166. data/lib/tasks/ruby_ui_admin.rake +71 -0
  167. data/public/ruby-ui-admin-assets/application.css +2 -0
  168. data/public/ruby-ui-admin-assets/controllers/index.js +36 -0
  169. data/public/ruby-ui-admin-assets/controllers/rua--bulk-select_controller.js +19 -0
  170. data/public/ruby-ui-admin-assets/controllers/rua--confirm_controller.js +51 -0
  171. data/public/ruby-ui-admin-assets/controllers/rua--dialog_controller.js +55 -0
  172. data/public/ruby-ui-admin-assets/controllers/rua--row-link_controller.js +16 -0
  173. data/public/ruby-ui-admin-assets/controllers/rua--tabs_controller.js +36 -0
  174. data/public/ruby-ui-admin-assets/controllers/ruby_ui--combobox_controller.js +197 -0
  175. data/public/ruby-ui-admin-assets/controllers/ruby_ui--sheet_content_controller.js +7 -0
  176. data/public/ruby-ui-admin-assets/controllers/ruby_ui--sheet_controller.js +15 -0
  177. data/public/ruby-ui-admin-assets/controllers/ruby_ui--sidebar_controller.js +67 -0
  178. data/public/ruby-ui-admin-assets/controllers/ruby_ui--toast_controller.js +151 -0
  179. data/public/ruby-ui-admin-assets/controllers/ruby_ui--toaster_controller.js +314 -0
  180. data/public/ruby-ui-admin-assets/vendor/stimulus.js +2567 -0
  181. data/public/ruby-ui-admin-assets/vendor/turbo.js +7178 -0
  182. metadata +379 -0
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUIAdmin
4
+ module Fields
5
+ class BelongsToField < BaseField
6
+ register_as :belongs_to
7
+
8
+ # belongs_to writes/reads the foreign key column.
9
+ def database_id
10
+ :"#{id}_id"
11
+ end
12
+
13
+ def foreign_key_value(record)
14
+ record.public_send(database_id) if record&.respond_to?(database_id)
15
+ end
16
+
17
+ # The ActiveRecord reflection for this association on the resource model.
18
+ def reflection
19
+ resource&.model_class&.reflect_on_association(id)
20
+ end
21
+
22
+ def target_model
23
+ options[:model] || reflection&.klass
24
+ end
25
+
26
+ # Candidate records to choose from in forms. Capped to avoid huge selects.
27
+ def options_for_select(limit: 1000)
28
+ model = target_model
29
+ return [] if model.nil?
30
+
31
+ model.all.limit(limit).map { |record| [display_label(record), record.id] }
32
+ end
33
+
34
+ def display_label(record)
35
+ return nil if record.nil?
36
+
37
+ if record.respond_to?(:to_label)
38
+ record.to_label
39
+ elsif record.respond_to?(:name)
40
+ record.name
41
+ elsif record.respond_to?(:title)
42
+ record.title
43
+ else
44
+ "#{record.model_name.human} ##{record.id}"
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUIAdmin
4
+ module Fields
5
+ class BooleanField < BaseField
6
+ register_as :boolean
7
+
8
+ def checked?(record)
9
+ !!value(record)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUIAdmin
4
+ module Fields
5
+ # A group of checkboxes whose value is a Hash of `{ key => boolean }`.
6
+ # field :feature_flags, as: :boolean_group, options: { "beta" => "Beta", "pro" => "Pro" }
7
+ class BooleanGroupField < BaseField
8
+ register_as :boolean_group
9
+
10
+ def default_hidden_views
11
+ %i[index]
12
+ end
13
+
14
+ # `{ key => label }`. `options:` may be a Hash, an Array of keys, or a lambda.
15
+ def group_options
16
+ raw = options[:options] || {}
17
+ raw = ExecutionContext.new(target: raw, resource: resource).handle if raw.respond_to?(:call)
18
+
19
+ raw.is_a?(Hash) ? raw.transform_keys(&:to_s) : Array(raw).to_h { |key| [key.to_s, key.to_s.humanize] }
20
+ end
21
+
22
+ def checked?(record, key)
23
+ value = value(record)
24
+ return false unless value.respond_to?(:[])
25
+
26
+ truthy?(value[key.to_s].nil? ? value[key.to_sym] : value[key.to_s])
27
+ end
28
+
29
+ def enabled_labels(record)
30
+ group_options.select { |key, _label| checked?(record, key) }.values
31
+ end
32
+
33
+ # Hash param: permit arbitrary nested keys.
34
+ def permit_param
35
+ {permitted_param => {}}
36
+ end
37
+
38
+ # Build the full { key => bool } hash from the submitted (checked-only) params.
39
+ def fill_value(record, submitted)
40
+ submitted ||= {}
41
+ result = group_options.keys.to_h { |key| [key, truthy?(submitted[key])] }
42
+
43
+ setter = "#{database_id}="
44
+ record.public_send(setter, result) if record.respond_to?(setter)
45
+ end
46
+
47
+ private
48
+
49
+ def truthy?(value)
50
+ ["1", "true", true].include?(value)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUIAdmin
4
+ module Fields
5
+ class CodeField < BaseField
6
+ register_as :code
7
+
8
+ def language
9
+ options[:language]
10
+ end
11
+
12
+ # Code can be large; keep it off the index by default.
13
+ def default_hidden_views
14
+ %i[index]
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUIAdmin
4
+ module Fields
5
+ class DateField < BaseField
6
+ register_as :date
7
+
8
+ DEFAULT_FORMAT = "%Y-%m-%d"
9
+
10
+ def format
11
+ options[:format] || DEFAULT_FORMAT
12
+ end
13
+
14
+ def formatted_value(record, view_context: nil)
15
+ raw = value(record, view_context: view_context)
16
+ return nil if raw.nil?
17
+ return super if @format_using
18
+ return raw unless raw.respond_to?(:strftime)
19
+
20
+ fmt = options[:format]
21
+ # A String `format:` is an explicit strftime pattern; otherwise localize via I18n.l
22
+ # (Symbol = named locale format, nil = gem's locale-aware default) so pt-BR renders
23
+ # "%d/%m/%Y", en "%Y-%m-%d", etc.
24
+ return raw.strftime(fmt) if fmt.is_a?(String)
25
+
26
+ begin
27
+ I18n.l(raw, format: fmt || :ruby_ui_admin)
28
+ rescue
29
+ raw.strftime(DEFAULT_FORMAT)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUIAdmin
4
+ module Fields
5
+ class DateTimeField < BaseField
6
+ register_as :date_time
7
+
8
+ DEFAULT_FORMAT = "%Y-%m-%d %H:%M"
9
+
10
+ def format
11
+ options[:format] || DEFAULT_FORMAT
12
+ end
13
+
14
+ def formatted_value(record, view_context: nil)
15
+ raw = value(record, view_context: view_context)
16
+ return nil if raw.nil?
17
+ return super if @format_using
18
+
19
+ # Display in the admin's configured timezone (set per-request from config.timezone).
20
+ raw = raw.in_time_zone if raw.respond_to?(:in_time_zone)
21
+ return raw unless raw.respond_to?(:strftime)
22
+
23
+ fmt = options[:format]
24
+ # A String `format:` is an explicit strftime pattern; otherwise localize via I18n.l
25
+ # (a Symbol picks a named locale format; nil uses the gem's locale-aware default), so
26
+ # pt-BR renders "%d/%m/%Y %H:%M", en "%Y-%m-%d %H:%M", etc.
27
+ return raw.strftime(fmt) if fmt.is_a?(String)
28
+
29
+ begin
30
+ I18n.l(raw, format: fmt || :ruby_ui_admin)
31
+ rescue
32
+ raw.strftime(DEFAULT_FORMAT)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUIAdmin
4
+ module Fields
5
+ # Registry mapping a DSL symbol (`as: :text`) to a field class.
6
+ # Each field subclass registers itself via `register_as`.
7
+ @mapping = {}
8
+
9
+ class << self
10
+ attr_reader :mapping
11
+
12
+ def register(key, klass)
13
+ @mapping[key.to_sym] = klass
14
+ end
15
+
16
+ def field_class_for(key)
17
+ @mapping.fetch(key.to_sym) do
18
+ raise ArgumentError, "Unknown field type :#{key}. Registered: #{@mapping.keys.join(", ")}"
19
+ end
20
+ end
21
+
22
+ def registered?(key)
23
+ @mapping.key?(key.to_sym)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUIAdmin
4
+ module Fields
5
+ # Single ActiveStorage attachment.
6
+ class FileField < BaseField
7
+ register_as :file
8
+
9
+ def default_hidden_views
10
+ %i[index]
11
+ end
12
+
13
+ def multiple?
14
+ false
15
+ end
16
+
17
+ def accept
18
+ options[:accept]
19
+ end
20
+
21
+ # Max display size (W×H, px) for an image thumbnail on the show view. Accepts an Integer
22
+ # (square, e.g. `preview_size: 100`) or `[width, height]` (e.g. `[120, 80]`). Applied as a
23
+ # max-width/max-height (the image scales down to fit, preserving aspect). Defaults to 64×64.
24
+ def preview_size
25
+ options[:preview_size]
26
+ end
27
+
28
+ def attachment(record)
29
+ record.public_send(id) if record.respond_to?(id)
30
+ end
31
+
32
+ def attached?(record)
33
+ att = attachment(record)
34
+ att.respond_to?(:attached?) && att.attached?
35
+ end
36
+
37
+ # Skip empty uploads so we don't blow away an existing attachment.
38
+ def fill_value(record, value)
39
+ return if value.blank?
40
+
41
+ setter = "#{id}="
42
+ record.public_send(setter, value) if record.respond_to?(setter)
43
+ end
44
+
45
+ # Form param for the "remove current file" checkbox.
46
+ def remove_param
47
+ :"#{permitted_param}_remove"
48
+ end
49
+
50
+ # Also permit the remove checkbox alongside the upload.
51
+ def permit_params
52
+ [permit_param, remove_param]
53
+ end
54
+
55
+ # Purge the current attachment when "remove" is checked; otherwise assign the upload
56
+ # (skipping blank so an empty input doesn't wipe an existing attachment).
57
+ def fill(record, attributes)
58
+ if ActiveModel::Type::Boolean.new.cast(attributes[remove_param.to_s])
59
+ record.public_send(id).purge if attached?(record)
60
+ return
61
+ end
62
+
63
+ super
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUIAdmin
4
+ module Fields
5
+ # Multiple ActiveStorage attachments (has_many_attached).
6
+ class FilesField < FileField
7
+ register_as :files
8
+
9
+ def multiple?
10
+ true
11
+ end
12
+
13
+ # Submitted as an array (record[avatars][]).
14
+ def permit_param
15
+ {permitted_param => []}
16
+ end
17
+
18
+ # Form param holding the ids of attachments marked for removal.
19
+ def remove_ids_param
20
+ :"#{permitted_param}_remove_ids"
21
+ end
22
+
23
+ # Permit the upload array plus the array of attachment ids to remove.
24
+ def permit_params
25
+ [permit_param, {remove_ids_param => []}]
26
+ end
27
+
28
+ def attachments(record)
29
+ att = attachment(record)
30
+ return [] unless att.respond_to?(:attached?) && att.attached?
31
+
32
+ att
33
+ end
34
+
35
+ # Purge the attachments the user checked, then APPEND any new uploads (unlike the single
36
+ # `:file`, new files don't replace the existing set). Both skip blanks/empties.
37
+ def fill(record, attributes)
38
+ purge_marked(record, attributes[remove_ids_param.to_s])
39
+ attach_new(record, attributes[permitted_param.to_s])
40
+ end
41
+
42
+ private
43
+
44
+ def purge_marked(record, ids)
45
+ ids = Array(ids).map(&:to_s).reject(&:blank?)
46
+ return if ids.empty?
47
+
48
+ collection = record.public_send(id)
49
+ return unless collection.respond_to?(:each)
50
+
51
+ collection.each { |att| att.purge if ids.include?(att.id.to_s) }
52
+ end
53
+
54
+ def attach_new(record, files)
55
+ files = Array(files).reject(&:blank?)
56
+ return if files.empty?
57
+
58
+ record.public_send(id).attach(files)
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUIAdmin
4
+ module Fields
5
+ class HasAndBelongsToManyField < HasManyField
6
+ register_as :has_and_belongs_to_many
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUIAdmin
4
+ module Fields
5
+ class HasManyField < AssociationField
6
+ register_as :has_many
7
+
8
+ # Capped collection of associated records for display on the show view.
9
+ def associated_records(record, limit: 25)
10
+ collection = scoped(value(record))
11
+ return [] if collection.nil?
12
+
13
+ collection.respond_to?(:limit) ? collection.limit(limit) : Array(collection).first(limit)
14
+ end
15
+
16
+ def count(record)
17
+ collection = scoped(value(record))
18
+ return 0 if collection.nil?
19
+
20
+ collection.respond_to?(:count) ? collection.count : Array(collection).size
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUIAdmin
4
+ module Fields
5
+ class HasOneField < AssociationField
6
+ register_as :has_one
7
+
8
+ def associated_record(record)
9
+ value(record)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUIAdmin
4
+ module Fields
5
+ # Renders only as a hidden input on forms; not shown on index/show.
6
+ class HiddenField < BaseField
7
+ register_as :hidden
8
+
9
+ def default_hidden_views
10
+ %i[index show]
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUIAdmin
4
+ module Fields
5
+ class IdField < BaseField
6
+ register_as :id
7
+
8
+ def initialize(id = :id, **options, &block)
9
+ super
10
+ @link_to_record = options.fetch(:link_to_record, true)
11
+ end
12
+
13
+ # The id isn't editable, so keep it off the new/edit forms.
14
+ def default_hidden_views
15
+ %i[new edit]
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module RubyUIAdmin
6
+ module Fields
7
+ # Edits a Hash value. Displayed as a key/value table; edited as JSON in a textarea.
8
+ class KeyValueField < BaseField
9
+ register_as :key_value
10
+
11
+ def default_hidden_views
12
+ %i[index]
13
+ end
14
+
15
+ def value_as_json(record)
16
+ raw = value(record)
17
+ return "{}" if raw.blank?
18
+
19
+ JSON.pretty_generate(raw)
20
+ rescue
21
+ raw.to_s
22
+ end
23
+
24
+ # Parse the submitted JSON back into a Hash before assigning.
25
+ def fill_value(record, value)
26
+ parsed =
27
+ if value.is_a?(String) && value.present?
28
+ begin
29
+ JSON.parse(value)
30
+ rescue JSON::ParserError
31
+ value
32
+ end
33
+ else
34
+ value
35
+ end
36
+
37
+ super(record, parsed)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUIAdmin
4
+ module Fields
5
+ class NumberField < BaseField
6
+ register_as :number
7
+
8
+ def step = options[:step]
9
+ def min = options[:min]
10
+ def max = options[:max]
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUIAdmin
4
+ module Fields
5
+ # Password input on forms; never displays the stored value.
6
+ class PasswordField < BaseField
7
+ register_as :password
8
+
9
+ def default_hidden_views
10
+ %i[index show]
11
+ end
12
+
13
+ # Don't write a blank password back onto the record.
14
+ def fill_value(record, value)
15
+ return if value.blank?
16
+
17
+ super
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUIAdmin
4
+ module Fields
5
+ # Renders a link to another record, resolved through a named resource:
6
+ # field :detail, as: :record_link, use_resource: "FormGroup", only_on: :show
7
+ class RecordLinkField < BaseField
8
+ register_as :record_link
9
+
10
+ def default_hidden_views
11
+ %i[new edit]
12
+ end
13
+
14
+ def use_resource
15
+ options[:use_resource]
16
+ end
17
+
18
+ def linked_record(record)
19
+ value(record)
20
+ end
21
+
22
+ def display_label(record)
23
+ return nil if record.nil?
24
+
25
+ if record.respond_to?(:to_label) then record.to_label
26
+ elsif record.respond_to?(:name) then record.name
27
+ elsif record.respond_to?(:title) then record.title
28
+ else "#{record.model_name.human} ##{record.id}"
29
+ end
30
+ end
31
+
32
+ # The resource class used to build the link.
33
+ def target_resource(linked = nil)
34
+ if use_resource
35
+ RubyUIAdmin.resource_manager.resources.find { |r| r.name.to_s.demodulize == use_resource.to_s } ||
36
+ (Object.const_get("RubyUIAdmin::Resources::#{use_resource}") rescue nil)
37
+ elsif linked
38
+ RubyUIAdmin.resource_manager.find_for_model(linked.class)
39
+ end
40
+ end
41
+
42
+ def fill_value(record, value); end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUIAdmin
4
+ module Fields
5
+ class SelectField < BaseField
6
+ register_as :select
7
+
8
+ # Resolves the configured options into an array of [label, value] pairs.
9
+ # `options:` accepts an Array, a Hash ({value => label}), or a lambda.
10
+ # `enum:` derives options from an ActiveRecord enum: pass the enum mapping
11
+ # (`enum: Post.statuses`) or `enum: true` to read `model_class.<id.pluralize>`.
12
+ def select_options(record = nil)
13
+ return enum_options if enum_options
14
+
15
+ raw = options[:options]
16
+ if raw.respond_to?(:call)
17
+ # The options lambda can use `record`/`resource` plus `params`/`view` (e.g. an
18
+ # options list that depends on the request, like a nested-create parent id).
19
+ raw = ExecutionContext.new(
20
+ target: raw,
21
+ record: record,
22
+ resource: resource,
23
+ params: resource&.params,
24
+ view: View.wrap(resource&.view)
25
+ ).handle
26
+ end
27
+
28
+ pairs =
29
+ case raw
30
+ when Hash then raw.map { |value, label| [label, value] }
31
+ when Array then raw.map { |entry| entry.is_a?(Array) ? entry : [entry, entry] }
32
+ else []
33
+ end
34
+
35
+ # `display_with_value: true` -> option labels become "Label (value)".
36
+ return pairs.map { |(label, value)| ["#{label} (#{value})", value] } if options[:display_with_value]
37
+
38
+ pairs
39
+ end
40
+
41
+ def include_blank?
42
+ options.fetch(:include_blank, !required?)
43
+ end
44
+
45
+ # `[label, value]` pairs derived from an ActiveRecord enum, or nil when no `enum:`.
46
+ def enum_options
47
+ enum = options[:enum]
48
+ return nil if enum.nil? || enum == false
49
+
50
+ mapping =
51
+ if enum == true
52
+ resource&.model_class&.public_send(id.to_s.pluralize)
53
+ else
54
+ enum
55
+ end
56
+ return nil unless mapping.respond_to?(:keys)
57
+
58
+ mapping.keys.map { |key| [key.to_s.humanize, key.to_s] }
59
+ end
60
+
61
+ # Display label for the current value.
62
+ def formatted_value(record, view_context: nil)
63
+ raw = value(record, view_context: view_context)
64
+ return super if @format_using
65
+
66
+ pair = select_options(record).find { |(_label, val)| val.to_s == raw.to_s }
67
+ pair ? pair.first : raw
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUIAdmin
4
+ module Fields
5
+ # Renders the value as a status badge with a leading dot. Configure with
6
+ # `options: { success: %w[done], warning: %w[pending], danger: %w[failed] }`.
7
+ class StatusField < BaseField
8
+ register_as :status
9
+
10
+ VARIANT_FOR_STATE = {success: :success, warning: :warning, danger: :destructive, info: :primary}.freeze
11
+
12
+ def variant_for(record)
13
+ current = value(record).to_s
14
+ states = options[:options] || {}
15
+
16
+ state = states.find { |_key, values| Array(values).map(&:to_s).include?(current) }&.first
17
+ VARIANT_FOR_STATE.fetch(state&.to_sym, :gray)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUIAdmin
4
+ module Fields
5
+ class TextField < BaseField
6
+ register_as :text
7
+
8
+ # Render the raw value as HTML (`as_html: true`).
9
+ def as_html?
10
+ !!options[:as_html]
11
+ end
12
+ end
13
+ end
14
+ end