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,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUIAdmin
|
|
4
|
+
module Fields
|
|
5
|
+
class UrlField < BaseField
|
|
6
|
+
register_as :url
|
|
7
|
+
|
|
8
|
+
# Optional display text; defaults to the URL itself.
|
|
9
|
+
def link_text(record)
|
|
10
|
+
text = options[:text]
|
|
11
|
+
return ExecutionContext.new(target: text, record: record, resource: resource).handle if text.respond_to?(:call)
|
|
12
|
+
|
|
13
|
+
text || value(record)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def target
|
|
17
|
+
options[:target] || "_blank"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUIAdmin
|
|
4
|
+
module Filters
|
|
5
|
+
# Base class for filters. Subclasses implement `apply(request, query, value)`
|
|
6
|
+
# and (for select-style filters) `options`.
|
|
7
|
+
class BaseFilter
|
|
8
|
+
class << self
|
|
9
|
+
# Class-level `self.name =` while still falling back to the real class
|
|
10
|
+
# name (so Zeitwerk/Rails keep working when no display name is set).
|
|
11
|
+
def name=(value)
|
|
12
|
+
@display_name = value
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def name
|
|
16
|
+
@display_name || super
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def empty_message=(value)
|
|
20
|
+
@empty_message = value
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def empty_message
|
|
24
|
+
defined?(@empty_message) ? @empty_message : nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Inherited down the chain (subclasses of SelectFilter are :select, etc.).
|
|
28
|
+
def filter_type
|
|
29
|
+
return @filter_type if defined?(@filter_type) && @filter_type
|
|
30
|
+
return superclass.filter_type if superclass.respond_to?(:filter_type)
|
|
31
|
+
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def register_type(type)
|
|
36
|
+
@filter_type = type.to_sym
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
attr_reader :arguments
|
|
41
|
+
|
|
42
|
+
def initialize(arguments: {})
|
|
43
|
+
@arguments = arguments || {}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def name
|
|
47
|
+
self.class.name
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def type
|
|
51
|
+
self.class.filter_type
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def empty_message
|
|
55
|
+
self.class.empty_message
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Unique key used in the query string, e.g.
|
|
59
|
+
# RubyUIAdmin::Filters::PublishedFilter -> "published_filter".
|
|
60
|
+
# Uses `to_s` (not the overridable `name`) to get the real class name.
|
|
61
|
+
def param_key
|
|
62
|
+
self.class.to_s.sub(/^RubyUIAdmin::Filters::/, "").gsub("::", "_").underscore
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Override in subclasses.
|
|
66
|
+
def apply(request, query, value)
|
|
67
|
+
query
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Override in select/multiple-select filters.
|
|
71
|
+
def options
|
|
72
|
+
{}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Override to provide a default selected value.
|
|
76
|
+
def default
|
|
77
|
+
nil
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUIAdmin
|
|
4
|
+
module Filters
|
|
5
|
+
# A filter offering several checkboxes; the submitted value is an Array of the
|
|
6
|
+
# checked option keys. Subclasses define `options` and `apply(request, query, value)`.
|
|
7
|
+
class MultipleSelectFilter < BaseFilter
|
|
8
|
+
register_type :multiple_select
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUIAdmin
|
|
4
|
+
module Menu
|
|
5
|
+
# A leaf linking to a resource's index. Carries the resolved resource so the view
|
|
6
|
+
# can build the path through the dynamic routes.
|
|
7
|
+
ResourceItem = Struct.new(:resource, :label, keyword_init: true) do
|
|
8
|
+
def type = :resource
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# A leaf linking to an arbitrary path.
|
|
12
|
+
LinkItem = Struct.new(:label, :path, keyword_init: true) do
|
|
13
|
+
def type = :link
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# A leaf linking to a dashboard.
|
|
17
|
+
DashboardItem = Struct.new(:dashboard, :label, keyword_init: true) do
|
|
18
|
+
def type = :dashboard
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# A titled group of items.
|
|
22
|
+
SectionItem = Struct.new(:label, :icon, :items, keyword_init: true) do
|
|
23
|
+
def type = :section
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Evaluates a `config.main_menu` block into a tree of menu items. The block is
|
|
27
|
+
# instance_exec'd against a Builder, so `section`/`resource`/`link`/... are DSL calls.
|
|
28
|
+
#
|
|
29
|
+
# config.main_menu = -> do
|
|
30
|
+
# section "Tables", icon: "table" do
|
|
31
|
+
# resource :user
|
|
32
|
+
# resource :buyer, label: "Buyers"
|
|
33
|
+
# link "Docs", "https://example.com"
|
|
34
|
+
# end
|
|
35
|
+
# end
|
|
36
|
+
class Builder
|
|
37
|
+
def self.build(&block)
|
|
38
|
+
builder = new
|
|
39
|
+
builder.instance_exec(&block)
|
|
40
|
+
builder.items
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
attr_reader :items
|
|
44
|
+
|
|
45
|
+
def initialize
|
|
46
|
+
@items = []
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def section(label = nil, icon: nil, &block)
|
|
50
|
+
children = self.class.build(&block)
|
|
51
|
+
@items << SectionItem.new(label: label, icon: icon, items: children)
|
|
52
|
+
end
|
|
53
|
+
alias_method :group, :section
|
|
54
|
+
|
|
55
|
+
def resource(name, label: nil, **)
|
|
56
|
+
resource = RubyUIAdmin.resource_manager.find_by_route_key(name.to_s)
|
|
57
|
+
@items << ResourceItem.new(resource: resource, label: label) if resource
|
|
58
|
+
end
|
|
59
|
+
alias_method :resources, :resource
|
|
60
|
+
|
|
61
|
+
def link(label, path = nil, **args)
|
|
62
|
+
@items << LinkItem.new(label: label, path: path || args[:path])
|
|
63
|
+
end
|
|
64
|
+
alias_method :link_to, :link
|
|
65
|
+
|
|
66
|
+
def dashboard(id, label: nil)
|
|
67
|
+
dash = RubyUIAdmin.dashboard_manager.dashboards.find { |d| d.id.to_s == id.to_s }
|
|
68
|
+
@items << DashboardItem.new(dashboard: dash, label: label) if dash
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def all_resources(except: [])
|
|
72
|
+
excluded = Array(except).map(&:to_s)
|
|
73
|
+
RubyUIAdmin.resource_manager.navigation_resources.each do |rc|
|
|
74
|
+
next if excluded.include?(rc.route_key)
|
|
75
|
+
|
|
76
|
+
@items << ResourceItem.new(resource: rc, label: nil)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def all_dashboards(except: [])
|
|
81
|
+
excluded = Array(except).map(&:to_s)
|
|
82
|
+
RubyUIAdmin.dashboard_manager.dashboards.each do |dash|
|
|
83
|
+
next if excluded.include?(dash.id.to_s)
|
|
84
|
+
|
|
85
|
+
@items << DashboardItem.new(dashboard: dash, label: nil)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Available inside the menu block.
|
|
90
|
+
def current_user = RubyUIAdmin::Current.user
|
|
91
|
+
def params = RubyUIAdmin::Current.params
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/core_ext/class/subclasses"
|
|
4
|
+
|
|
5
|
+
module RubyUIAdmin
|
|
6
|
+
# Discovers and indexes the host app's resources (defined under
|
|
7
|
+
# `RubyUIAdmin::Resources` in `app/ruby_ui_admin/resources`).
|
|
8
|
+
class ResourceManager
|
|
9
|
+
def boot
|
|
10
|
+
@resources = nil
|
|
11
|
+
self
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def resources
|
|
15
|
+
@resources ||= fetch_resources
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# All resources visible in the sidebar navigation.
|
|
19
|
+
def navigation_resources
|
|
20
|
+
resources.select { |resource| resource.visible_on_sidebar }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Find a resource class by its route key, e.g. "buyers".
|
|
24
|
+
def find_by_route_key(key)
|
|
25
|
+
key = key.to_s
|
|
26
|
+
resources.find { |resource| resource.route_key == key } ||
|
|
27
|
+
resources.find { |resource| resource.resource_name == key.singularize }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Find the resource class for a given model class.
|
|
31
|
+
def find_for_model(model_class)
|
|
32
|
+
mapped = RubyUIAdmin.configuration.model_resource_mapping[model_class.to_s]
|
|
33
|
+
return mapped.to_s.constantize if mapped
|
|
34
|
+
|
|
35
|
+
resources.find { |resource| resource.model_class == model_class }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def fetch_resources
|
|
41
|
+
eager_load_resources!
|
|
42
|
+
|
|
43
|
+
selected = RubyUIAdmin::BaseResource.descendants.select do |resource|
|
|
44
|
+
next false if resource.abstract?
|
|
45
|
+
|
|
46
|
+
# Only real, named resources count — excludes anonymous subclasses (e.g.
|
|
47
|
+
# those built in tests) that would otherwise pollute the registry.
|
|
48
|
+
name = resource.name
|
|
49
|
+
name && (RubyUIAdmin.configuration.resources.present? || name.start_with?("RubyUIAdmin::Resources::"))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Dedupe by name and re-resolve the constant: in development, `descendants` retains
|
|
53
|
+
# stale class versions from previous Zeitwerk reloads (same name) — keep the current one.
|
|
54
|
+
selected.map(&:name).uniq.filter_map { |name| name.safe_constantize }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def eager_load_resources!
|
|
58
|
+
configured = RubyUIAdmin.configuration.resources
|
|
59
|
+
|
|
60
|
+
if configured.present?
|
|
61
|
+
Array(configured).each { |name| name.to_s.constantize }
|
|
62
|
+
elsif defined?(::Rails)
|
|
63
|
+
eager_load_resources_dir
|
|
64
|
+
end
|
|
65
|
+
rescue NameError
|
|
66
|
+
nil
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def eager_load_resources_dir
|
|
70
|
+
dir = Rails.root.join("app", "ruby_ui_admin", "resources")
|
|
71
|
+
return unless Dir.exist?(dir)
|
|
72
|
+
|
|
73
|
+
loader = Rails.autoloaders.main
|
|
74
|
+
if loader.respond_to?(:eager_load_dir)
|
|
75
|
+
loader.eager_load_dir(dir)
|
|
76
|
+
else
|
|
77
|
+
# Fallback for older Zeitwerk: reference the namespace to trigger loading.
|
|
78
|
+
loader.eager_load_namespace(RubyUIAdmin::Resources) if defined?(RubyUIAdmin::Resources)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUIAdmin
|
|
4
|
+
module Scopes
|
|
5
|
+
# Base class for named index scopes. A scope narrows the index query
|
|
6
|
+
# via a `scope` lambda and shows up as a tab above the table.
|
|
7
|
+
#
|
|
8
|
+
# class Published < RubyUIAdmin::Scopes::BaseScope
|
|
9
|
+
# self.name = "Published"
|
|
10
|
+
# self.scope = -> { query.where(published: true) }
|
|
11
|
+
# self.default = true
|
|
12
|
+
# end
|
|
13
|
+
class BaseScope
|
|
14
|
+
class << self
|
|
15
|
+
# Class-level `self.name =` with fallback to the real class name.
|
|
16
|
+
def name=(value)
|
|
17
|
+
@display_name = value
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def name
|
|
21
|
+
@display_name || super
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def scope=(value)
|
|
25
|
+
@scope = value
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def scope
|
|
29
|
+
@scope if defined?(@scope)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def description=(value)
|
|
33
|
+
@description = value
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def description
|
|
37
|
+
@description if defined?(@description)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def default=(value)
|
|
41
|
+
@default = value
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def default?
|
|
45
|
+
!!(@default if defined?(@default))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def visible=(value)
|
|
49
|
+
@visible = value
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def visible_setting
|
|
53
|
+
defined?(@visible) ? @visible : true
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# URL key, e.g. RubyUIAdmin::Scopes::PublishedPosts -> "published_posts".
|
|
57
|
+
def scope_key
|
|
58
|
+
to_s.sub(/^RubyUIAdmin::Scopes::/, "").gsub("::", "_").underscore
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
attr_reader :params
|
|
63
|
+
|
|
64
|
+
# `default_override` (set via `scope Klass, default: true` on the resource) wins over
|
|
65
|
+
# the scope class's own `self.default`.
|
|
66
|
+
def initialize(params: {}, default_override: nil)
|
|
67
|
+
@params = params || {}
|
|
68
|
+
@default_override = default_override
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def name
|
|
72
|
+
self.class.name
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def key
|
|
76
|
+
self.class.scope_key
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def description
|
|
80
|
+
self.class.description
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def default?
|
|
84
|
+
return !!@default_override unless @default_override.nil?
|
|
85
|
+
|
|
86
|
+
self.class.default?
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Applies the scope to the query. A Symbol calls the model's named scope/class method;
|
|
90
|
+
# a lambda is run with `query` and `params` available.
|
|
91
|
+
def apply(query)
|
|
92
|
+
scope = self.class.scope
|
|
93
|
+
return query if scope.nil?
|
|
94
|
+
return query.public_send(scope) if scope.is_a?(Symbol)
|
|
95
|
+
|
|
96
|
+
ExecutionContext.new(target: scope, query: query, params: params).handle
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def visible?(user: nil)
|
|
100
|
+
setting = self.class.visible_setting
|
|
101
|
+
return setting unless setting.respond_to?(:call)
|
|
102
|
+
|
|
103
|
+
ExecutionContext.new(target: setting, user: user, params: params).handle
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUIAdmin
|
|
4
|
+
module Services
|
|
5
|
+
# Facade over the configured authorization adapter (`config.authorization_client`, default
|
|
6
|
+
# action_policy). Resolves the adapter and delegates, so controllers/views/resources stay
|
|
7
|
+
# backend-agnostic. The backend logic lives in `RubyUIAdmin::Authorization::*Adapter`.
|
|
8
|
+
class AuthorizationService
|
|
9
|
+
def initialize(user, record = nil, policy_class: nil, true_user: nil)
|
|
10
|
+
@adapter = RubyUIAdmin::Authorization.build(user, record, policy_class: policy_class, true_user: true_user)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def authorize_action(rule, record: nil, raise_exception: true)
|
|
14
|
+
@adapter.authorize_action(rule, record: record, raise_exception: raise_exception)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def allowed?(rule, record: nil) = @adapter.allowed?(rule, record: record)
|
|
18
|
+
|
|
19
|
+
def apply_policy(scope) = @adapter.apply_policy(scope)
|
|
20
|
+
|
|
21
|
+
def has_rule?(rule, record: nil) = @adapter.has_rule?(rule, record: record)
|
|
22
|
+
|
|
23
|
+
def defines_rule?(rule) = @adapter.defines_rule?(rule)
|
|
24
|
+
|
|
25
|
+
def true_user = @adapter.true_user
|
|
26
|
+
|
|
27
|
+
def set_record(record)
|
|
28
|
+
@adapter.set_record(record)
|
|
29
|
+
self
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Context readers (kept for backward compatibility with any caller reading them).
|
|
33
|
+
def user = @adapter.user
|
|
34
|
+
def record = @adapter.record
|
|
35
|
+
def policy_class = @adapter.policy_class
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUIAdmin
|
|
4
|
+
# Layout containers produced by the resource DSL (`panel`, `tabs`, `tab`).
|
|
5
|
+
# They hold child items (fields or nested containers) for structured rendering
|
|
6
|
+
# on the show/form views.
|
|
7
|
+
module Structure
|
|
8
|
+
class Panel
|
|
9
|
+
attr_reader :name, :items
|
|
10
|
+
|
|
11
|
+
def initialize(name = nil)
|
|
12
|
+
@name = name
|
|
13
|
+
@items = []
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class TabGroup
|
|
18
|
+
attr_reader :tabs
|
|
19
|
+
|
|
20
|
+
def initialize
|
|
21
|
+
@tabs = []
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Containers expose their children through `items` for uniform traversal.
|
|
25
|
+
def items
|
|
26
|
+
@tabs
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class Tab
|
|
31
|
+
attr_reader :name, :description, :items
|
|
32
|
+
|
|
33
|
+
def initialize(name, description: nil)
|
|
34
|
+
@name = name
|
|
35
|
+
@description = description
|
|
36
|
+
@items = []
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module RubyUIAdmin
|
|
6
|
+
# Extracts the Tailwind class candidates used by the admin's Phlex views into a single file the
|
|
7
|
+
# host app can `@source` from its Tailwind build. This solves the "gem path changes per
|
|
8
|
+
# environment" problem: instead of pointing `@source` at the gem's `.rb` files (whose absolute
|
|
9
|
+
# path differs between dev/CI/deploy), the host writes the class inventory into its OWN tree (a
|
|
10
|
+
# stable, relative path) and re-runs on upgrade. It scans the same literals Tailwind would, so the
|
|
11
|
+
# generated utilities match the previous `@source "<gem>/**/*.rb"` approach.
|
|
12
|
+
#
|
|
13
|
+
# Driven by `rake ruby_ui_admin:tailwind_source`.
|
|
14
|
+
module TailwindSource
|
|
15
|
+
VIEWS_GLOB = "app/components/ruby_ui_admin/**/*.rb"
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
# All class candidates across the engine's view files, deduped and sorted.
|
|
20
|
+
def class_candidates
|
|
21
|
+
Dir[RubyUIAdmin::Engine.root.join(VIEWS_GLOB)].sort
|
|
22
|
+
.flat_map { |file| extract(File.read(file)) }
|
|
23
|
+
.uniq.sort
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Pulls Tailwind-ish class tokens out of a chunk of Ruby source: every double-quoted string
|
|
27
|
+
# literal (and heredoc `CLASSES` body) is split into tokens, with `#{…}` interpolations dropped
|
|
28
|
+
# so the literal fragments around them survive. Non-class words are filtered out by
|
|
29
|
+
# `class_token?`. Only double quotes are scanned — the views write every `class:` with them, and
|
|
30
|
+
# scanning single quotes would let stray apostrophes in comments span across real code.
|
|
31
|
+
def extract(ruby_source)
|
|
32
|
+
strings = ruby_source.scan(/"([^"]*)"/).flatten
|
|
33
|
+
strings += ruby_source.scan(/<<~?CLASSES\n(.*?)\n\s*CLASSES/m).flatten
|
|
34
|
+
|
|
35
|
+
strings
|
|
36
|
+
.map { |string| string.gsub(/#\{[^}]*\}/, " ") }
|
|
37
|
+
.flat_map { |string| string.split(/\s+/) }
|
|
38
|
+
.select { |token| class_token?(token) }
|
|
39
|
+
.uniq
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Heuristic: a Tailwind utility candidate. Must start with a letter (optionally `!`/`-` for
|
|
43
|
+
# important/negative) and may carry variants/values (`hover:`, `/10`, `[356px]`). Requiring a
|
|
44
|
+
# letter start drops embedded SVG path coordinates (e.g. `0-7.07l-1.72`), which start with a
|
|
45
|
+
# digit and would otherwise be scanned as junk — no real Tailwind utility starts with a digit.
|
|
46
|
+
def class_token?(token)
|
|
47
|
+
token.match?(%r{\A!?-?[a-z][a-z0-9:_/\[\]().,%#-]*\z})
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The scannable file body (valid HTML so any Tailwind extractor picks it up).
|
|
51
|
+
def html
|
|
52
|
+
<<~HTML
|
|
53
|
+
<!-- Generated by `rake ruby_ui_admin:tailwind_source`. Do not edit — re-run after upgrading ruby_ui_admin. -->
|
|
54
|
+
<div class="#{class_candidates.join(" ")}"></div>
|
|
55
|
+
HTML
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Writes the inventory to `dest` and returns the path.
|
|
59
|
+
def write(dest)
|
|
60
|
+
FileUtils.mkdir_p(File.dirname(dest))
|
|
61
|
+
File.write(dest, html)
|
|
62
|
+
dest
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUIAdmin
|
|
4
|
+
# A small value object for the current view, so user lambdas (`visible:`, action
|
|
5
|
+
# `self.visible`/`self.message`) can write either `view == :show` or `view.show?`.
|
|
6
|
+
# It behaves like its underlying Symbol for comparison.
|
|
7
|
+
class View
|
|
8
|
+
def self.wrap(value)
|
|
9
|
+
value.is_a?(View) ? value : new(value)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(name)
|
|
13
|
+
@name = name&.to_sym
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def to_sym = @name
|
|
17
|
+
def to_s = @name.to_s
|
|
18
|
+
def inspect = "#<RubyUIAdmin::View #{@name.inspect}>"
|
|
19
|
+
|
|
20
|
+
def index? = @name == :index
|
|
21
|
+
def show? = @name == :show
|
|
22
|
+
def new? = @name == :new
|
|
23
|
+
def edit? = @name == :edit
|
|
24
|
+
def form? = @name == :new || @name == :edit # new + edit
|
|
25
|
+
def display? = @name == :index || @name == :show # index + show
|
|
26
|
+
|
|
27
|
+
def ==(other)
|
|
28
|
+
other.respond_to?(:to_sym) ? @name == other.to_sym : false
|
|
29
|
+
end
|
|
30
|
+
alias_method :eql?, :==
|
|
31
|
+
|
|
32
|
+
def hash = @name.hash
|
|
33
|
+
end
|
|
34
|
+
end
|