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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +165 -0
- data/app/assets/stylesheets/ruby_ui_admin/application.tailwind.css +111 -0
- data/app/components/ruby_ui_admin/ui/base.rb +26 -0
- data/app/components/ruby_ui_admin/ui/icon.rb +79 -0
- data/app/components/ruby_ui_admin/ui/native_select.rb +37 -0
- data/app/components/ruby_ui_admin/ui/native_select_group.rb +18 -0
- data/app/components/ruby_ui_admin/ui/native_select_icon.rb +42 -0
- data/app/components/ruby_ui_admin/ui/native_select_option.rb +18 -0
- data/app/components/ruby_ui_admin/ui/pagination.rb +62 -0
- data/app/components/ruby_ui_admin/ui/pagination_content.rb +18 -0
- data/app/components/ruby_ui_admin/ui/pagination_ellipsis.rb +45 -0
- data/app/components/ruby_ui_admin/ui/pagination_item.rb +32 -0
- data/app/components/ruby_ui_admin/ui/select.rb +29 -0
- data/app/components/ruby_ui_admin/ui/toast_action.rb +30 -0
- data/app/components/ruby_ui_admin/ui/toast_cancel.rb +30 -0
- data/app/components/ruby_ui_admin/ui/toast_close.rb +43 -0
- data/app/components/ruby_ui_admin/ui/toast_description.rb +21 -0
- data/app/components/ruby_ui_admin/ui/toast_icon.rb +69 -0
- data/app/components/ruby_ui_admin/ui/toast_item.rb +75 -0
- data/app/components/ruby_ui_admin/ui/toast_region.rb +147 -0
- data/app/components/ruby_ui_admin/ui/toast_title.rb +21 -0
- data/app/components/ruby_ui_admin/ui.rb +12 -0
- data/app/components/ruby_ui_admin/views/action.rb +37 -0
- data/app/components/ruby_ui_admin/views/action_form.rb +104 -0
- data/app/components/ruby_ui_admin/views/action_trigger.rb +148 -0
- data/app/components/ruby_ui_admin/views/actions_menu.rb +34 -0
- data/app/components/ruby_ui_admin/views/base.rb +301 -0
- data/app/components/ruby_ui_admin/views/card_component.rb +69 -0
- data/app/components/ruby_ui_admin/views/dashboard.rb +40 -0
- data/app/components/ruby_ui_admin/views/docs.rb +91 -0
- data/app/components/ruby_ui_admin/views/field_input.rb +156 -0
- data/app/components/ruby_ui_admin/views/field_value.rb +301 -0
- data/app/components/ruby_ui_admin/views/filter_bar.rb +111 -0
- data/app/components/ruby_ui_admin/views/form.rb +151 -0
- data/app/components/ruby_ui_admin/views/home.rb +35 -0
- data/app/components/ruby_ui_admin/views/index.rb +324 -0
- data/app/components/ruby_ui_admin/views/path_helpers.rb +36 -0
- data/app/components/ruby_ui_admin/views/rails_helpers.rb +15 -0
- data/app/components/ruby_ui_admin/views/show.rb +77 -0
- data/app/components/ruby_ui_admin/views/show_fields.rb +37 -0
- data/app/components/ruby_ui_admin/views/show_tab.rb +58 -0
- data/app/components/ruby_ui_admin/views/structure_renderer.rb +148 -0
- data/app/components/ruby_ui_admin/views/translation.rb +12 -0
- data/app/controllers/ruby_ui_admin/actions_controller.rb +162 -0
- data/app/controllers/ruby_ui_admin/application_controller.rb +81 -0
- data/app/controllers/ruby_ui_admin/dashboards_controller.rb +12 -0
- data/app/controllers/ruby_ui_admin/docs_controller.rb +166 -0
- data/app/controllers/ruby_ui_admin/home_controller.rb +18 -0
- data/app/controllers/ruby_ui_admin/resources_controller.rb +259 -0
- data/config/locales/ruby_ui_admin.en.yml +59 -0
- data/config/locales/ruby_ui_admin.pt-BR.yml +59 -0
- data/config/routes/dynamic_routes.rb +11 -0
- data/config/routes.rb +23 -0
- data/docs/README.md +53 -0
- data/docs/actions/overview.md +112 -0
- data/docs/authorization/action-policy.md +72 -0
- data/docs/authorization/adapters.md +101 -0
- data/docs/authorization/explicit-mode.md +45 -0
- data/docs/authorization/field-authorization.md +53 -0
- data/docs/authorization/policies.md +62 -0
- data/docs/authorization/scopes.md +48 -0
- data/docs/customization/controllers.md +62 -0
- data/docs/customization/ejecting.md +55 -0
- data/docs/customization/javascript.md +161 -0
- data/docs/customization/theming-rubyui.md +80 -0
- data/docs/dashboards/overview.md +65 -0
- data/docs/fields/overview.md +94 -0
- data/docs/filters/overview.md +73 -0
- data/docs/generators-and-tasks/overview.md +42 -0
- data/docs/getting-started/authentication.md +98 -0
- data/docs/getting-started/configuration.md +174 -0
- data/docs/getting-started/installation.md +201 -0
- data/docs/getting-started/internationalization.md +50 -0
- data/docs/getting-started/practical-guide.md +310 -0
- data/docs/resources/associations.md +66 -0
- data/docs/resources/index-customization.md +68 -0
- data/docs/resources/overview.md +50 -0
- data/docs/resources/scopes.md +54 -0
- data/docs/resources/tabs-panels.md +65 -0
- data/lib/generators/ruby_ui_admin/action/action_generator.rb +21 -0
- data/lib/generators/ruby_ui_admin/action/templates/action.rb.tt +24 -0
- data/lib/generators/ruby_ui_admin/assets/assets_generator.rb +71 -0
- data/lib/generators/ruby_ui_admin/card/card_generator.rb +28 -0
- data/lib/generators/ruby_ui_admin/card/templates/chart_card.rb.tt +14 -0
- data/lib/generators/ruby_ui_admin/card/templates/metric_card.rb.tt +14 -0
- data/lib/generators/ruby_ui_admin/card/templates/partial_card.rb.tt +13 -0
- data/lib/generators/ruby_ui_admin/components/components_generator.rb +43 -0
- data/lib/generators/ruby_ui_admin/controller/controller_generator.rb +30 -0
- data/lib/generators/ruby_ui_admin/controller/templates/controller.rb.tt +24 -0
- data/lib/generators/ruby_ui_admin/dashboard/dashboard_generator.rb +18 -0
- data/lib/generators/ruby_ui_admin/dashboard/templates/dashboard.rb.tt +14 -0
- data/lib/generators/ruby_ui_admin/eject/eject_generator.rb +55 -0
- data/lib/generators/ruby_ui_admin/filter/filter_generator.rb +28 -0
- data/lib/generators/ruby_ui_admin/filter/templates/boolean_filter.rb.tt +13 -0
- data/lib/generators/ruby_ui_admin/filter/templates/select_filter.rb.tt +19 -0
- data/lib/generators/ruby_ui_admin/filter/templates/text_filter.rb.tt +13 -0
- data/lib/generators/ruby_ui_admin/install/install_generator.rb +62 -0
- data/lib/generators/ruby_ui_admin/install/templates/initializer.rb.tt +41 -0
- data/lib/generators/ruby_ui_admin/locales/locales_generator.rb +22 -0
- data/lib/generators/ruby_ui_admin/policy/policy_generator.rb +18 -0
- data/lib/generators/ruby_ui_admin/policy/templates/policy.rb.tt +24 -0
- data/lib/generators/ruby_ui_admin/resource/resource_generator.rb +84 -0
- data/lib/generators/ruby_ui_admin/resource/templates/resource.rb.tt +15 -0
- data/lib/generators/ruby_ui_admin/scope/scope_generator.rb +18 -0
- data/lib/generators/ruby_ui_admin/scope/templates/scope.rb.tt +11 -0
- data/lib/ruby_ui_admin/authorization/action_policy_adapter.rb +69 -0
- data/lib/ruby_ui_admin/authorization/adapter.rb +80 -0
- data/lib/ruby_ui_admin/authorization/can_can_can_adapter.rb +70 -0
- data/lib/ruby_ui_admin/authorization/pundit_adapter.rb +86 -0
- data/lib/ruby_ui_admin/authorization.rb +40 -0
- data/lib/ruby_ui_admin/base_action.rb +211 -0
- data/lib/ruby_ui_admin/base_dashboard.rb +70 -0
- data/lib/ruby_ui_admin/base_policy.rb +38 -0
- data/lib/ruby_ui_admin/base_resource.rb +503 -0
- data/lib/ruby_ui_admin/cards/base_card.rb +59 -0
- data/lib/ruby_ui_admin/cards/chart_card.rb +16 -0
- data/lib/ruby_ui_admin/cards/metric_card.rb +22 -0
- data/lib/ruby_ui_admin/cards/partial_card.rb +14 -0
- data/lib/ruby_ui_admin/configuration.rb +177 -0
- data/lib/ruby_ui_admin/current.rb +20 -0
- data/lib/ruby_ui_admin/dashboard_manager.rb +52 -0
- data/lib/ruby_ui_admin/engine.rb +80 -0
- data/lib/ruby_ui_admin/execution_context.rb +36 -0
- data/lib/ruby_ui_admin/fields/association_field.rb +67 -0
- data/lib/ruby_ui_admin/fields/badge_field.rb +19 -0
- data/lib/ruby_ui_admin/fields/base_field.rb +208 -0
- data/lib/ruby_ui_admin/fields/belongs_to_field.rb +49 -0
- data/lib/ruby_ui_admin/fields/boolean_field.rb +13 -0
- data/lib/ruby_ui_admin/fields/boolean_group_field.rb +54 -0
- data/lib/ruby_ui_admin/fields/code_field.rb +18 -0
- data/lib/ruby_ui_admin/fields/date_field.rb +34 -0
- data/lib/ruby_ui_admin/fields/date_time_field.rb +37 -0
- data/lib/ruby_ui_admin/fields/field_manager.rb +27 -0
- data/lib/ruby_ui_admin/fields/file_field.rb +67 -0
- data/lib/ruby_ui_admin/fields/files_field.rb +62 -0
- data/lib/ruby_ui_admin/fields/has_and_belongs_to_many_field.rb +9 -0
- data/lib/ruby_ui_admin/fields/has_many_field.rb +24 -0
- data/lib/ruby_ui_admin/fields/has_one_field.rb +13 -0
- data/lib/ruby_ui_admin/fields/hidden_field.rb +14 -0
- data/lib/ruby_ui_admin/fields/id_field.rb +19 -0
- data/lib/ruby_ui_admin/fields/key_value_field.rb +41 -0
- data/lib/ruby_ui_admin/fields/number_field.rb +13 -0
- data/lib/ruby_ui_admin/fields/password_field.rb +21 -0
- data/lib/ruby_ui_admin/fields/record_link_field.rb +45 -0
- data/lib/ruby_ui_admin/fields/select_field.rb +71 -0
- data/lib/ruby_ui_admin/fields/status_field.rb +21 -0
- data/lib/ruby_ui_admin/fields/text_field.rb +14 -0
- data/lib/ruby_ui_admin/fields/textarea_field.rb +13 -0
- data/lib/ruby_ui_admin/fields/url_field.rb +21 -0
- data/lib/ruby_ui_admin/filters/base_filter.rb +81 -0
- data/lib/ruby_ui_admin/filters/boolean_filter.rb +9 -0
- data/lib/ruby_ui_admin/filters/multiple_select_filter.rb +11 -0
- data/lib/ruby_ui_admin/filters/select_filter.rb +9 -0
- data/lib/ruby_ui_admin/filters/text_filter.rb +9 -0
- data/lib/ruby_ui_admin/menu/builder.rb +94 -0
- data/lib/ruby_ui_admin/resource_manager.rb +82 -0
- data/lib/ruby_ui_admin/scopes/base_scope.rb +107 -0
- data/lib/ruby_ui_admin/services/authorization_service.rb +38 -0
- data/lib/ruby_ui_admin/structure.rb +40 -0
- data/lib/ruby_ui_admin/tailwind_source.rb +65 -0
- data/lib/ruby_ui_admin/version.rb +5 -0
- data/lib/ruby_ui_admin/view.rb +34 -0
- data/lib/ruby_ui_admin.rb +110 -0
- data/lib/tasks/ruby_ui_admin.rake +71 -0
- data/public/ruby-ui-admin-assets/application.css +2 -0
- data/public/ruby-ui-admin-assets/controllers/index.js +36 -0
- data/public/ruby-ui-admin-assets/controllers/rua--bulk-select_controller.js +19 -0
- data/public/ruby-ui-admin-assets/controllers/rua--confirm_controller.js +51 -0
- data/public/ruby-ui-admin-assets/controllers/rua--dialog_controller.js +55 -0
- data/public/ruby-ui-admin-assets/controllers/rua--row-link_controller.js +16 -0
- data/public/ruby-ui-admin-assets/controllers/rua--tabs_controller.js +36 -0
- data/public/ruby-ui-admin-assets/controllers/ruby_ui--combobox_controller.js +197 -0
- data/public/ruby-ui-admin-assets/controllers/ruby_ui--sheet_content_controller.js +7 -0
- data/public/ruby-ui-admin-assets/controllers/ruby_ui--sheet_controller.js +15 -0
- data/public/ruby-ui-admin-assets/controllers/ruby_ui--sidebar_controller.js +67 -0
- data/public/ruby-ui-admin-assets/controllers/ruby_ui--toast_controller.js +151 -0
- data/public/ruby-ui-admin-assets/controllers/ruby_ui--toaster_controller.js +314 -0
- data/public/ruby-ui-admin-assets/vendor/stimulus.js +2567 -0
- data/public/ruby-ui-admin-assets/vendor/turbo.js +7178 -0
- 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,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,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,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,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
|