rsb-admin 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +15 -0
- data/README.md +83 -0
- data/Rakefile +25 -0
- data/app/assets/javascripts/rsb/admin/themes/modern.js +37 -0
- data/app/assets/stylesheets/rsb/admin/themes/default.css +1358 -0
- data/app/assets/stylesheets/rsb/admin/themes/modern.css +1370 -0
- data/app/controllers/concerns/rsb/admin/authorization.rb +21 -0
- data/app/controllers/rsb/admin/admin_controller.rb +138 -0
- data/app/controllers/rsb/admin/admin_users_controller.rb +110 -0
- data/app/controllers/rsb/admin/dashboard_controller.rb +76 -0
- data/app/controllers/rsb/admin/profile_controller.rb +146 -0
- data/app/controllers/rsb/admin/profile_sessions_controller.rb +45 -0
- data/app/controllers/rsb/admin/resources_controller.rb +386 -0
- data/app/controllers/rsb/admin/roles_controller.rb +99 -0
- data/app/controllers/rsb/admin/sessions_controller.rb +139 -0
- data/app/controllers/rsb/admin/settings_controller.rb +203 -0
- data/app/controllers/rsb/admin/two_factor_controller.rb +105 -0
- data/app/helpers/rsb/admin/authorization_helper.rb +49 -0
- data/app/helpers/rsb/admin/branding_helper.rb +38 -0
- data/app/helpers/rsb/admin/formatting_helper.rb +205 -0
- data/app/helpers/rsb/admin/i18n_helper.rb +148 -0
- data/app/helpers/rsb/admin/icons_helper.rb +55 -0
- data/app/helpers/rsb/admin/table_helper.rb +132 -0
- data/app/helpers/rsb/admin/theme_helper.rb +84 -0
- data/app/helpers/rsb/admin/url_helper.rb +109 -0
- data/app/mailers/rsb/admin/admin_mailer.rb +37 -0
- data/app/models/rsb/admin/admin_session.rb +109 -0
- data/app/models/rsb/admin/admin_user.rb +153 -0
- data/app/models/rsb/admin/application_record.rb +10 -0
- data/app/models/rsb/admin/role.rb +63 -0
- data/app/views/layouts/rsb/admin/application.html.erb +45 -0
- data/app/views/rsb/admin/admin_mailer/email_verification.html.erb +11 -0
- data/app/views/rsb/admin/admin_mailer/email_verification.text.erb +11 -0
- data/app/views/rsb/admin/admin_users/_form.html.erb +52 -0
- data/app/views/rsb/admin/admin_users/edit.html.erb +10 -0
- data/app/views/rsb/admin/admin_users/index.html.erb +77 -0
- data/app/views/rsb/admin/admin_users/new.html.erb +10 -0
- data/app/views/rsb/admin/admin_users/show.html.erb +85 -0
- data/app/views/rsb/admin/dashboard/index.html.erb +36 -0
- data/app/views/rsb/admin/profile/edit.html.erb +67 -0
- data/app/views/rsb/admin/profile/show.html.erb +155 -0
- data/app/views/rsb/admin/resources/_filters.html.erb +58 -0
- data/app/views/rsb/admin/resources/_form.html.erb +20 -0
- data/app/views/rsb/admin/resources/_pagination.html.erb +33 -0
- data/app/views/rsb/admin/resources/_table.html.erb +70 -0
- data/app/views/rsb/admin/resources/edit.html.erb +7 -0
- data/app/views/rsb/admin/resources/index.html.erb +49 -0
- data/app/views/rsb/admin/resources/new.html.erb +7 -0
- data/app/views/rsb/admin/resources/page.html.erb +9 -0
- data/app/views/rsb/admin/resources/show.html.erb +55 -0
- data/app/views/rsb/admin/roles/_form.html.erb +197 -0
- data/app/views/rsb/admin/roles/edit.html.erb +7 -0
- data/app/views/rsb/admin/roles/index.html.erb +71 -0
- data/app/views/rsb/admin/roles/new.html.erb +7 -0
- data/app/views/rsb/admin/roles/show.html.erb +99 -0
- data/app/views/rsb/admin/sessions/new.html.erb +31 -0
- data/app/views/rsb/admin/sessions/two_factor.html.erb +39 -0
- data/app/views/rsb/admin/settings/_field.html.erb +115 -0
- data/app/views/rsb/admin/settings/index.html.erb +61 -0
- data/app/views/rsb/admin/shared/_badge.html.erb +1 -0
- data/app/views/rsb/admin/shared/_breadcrumbs.html.erb +12 -0
- data/app/views/rsb/admin/shared/_empty_state.html.erb +4 -0
- data/app/views/rsb/admin/shared/_flash.html.erb +22 -0
- data/app/views/rsb/admin/shared/_header.html.erb +50 -0
- data/app/views/rsb/admin/shared/_page_tabs.html.erb +21 -0
- data/app/views/rsb/admin/shared/_sidebar.html.erb +99 -0
- data/app/views/rsb/admin/shared/disabled.html.erb +38 -0
- data/app/views/rsb/admin/shared/fields/_checkbox.html.erb +6 -0
- data/app/views/rsb/admin/shared/fields/_datetime.html.erb +10 -0
- data/app/views/rsb/admin/shared/fields/_email.html.erb +10 -0
- data/app/views/rsb/admin/shared/fields/_hidden.html.erb +1 -0
- data/app/views/rsb/admin/shared/fields/_json.html.erb +11 -0
- data/app/views/rsb/admin/shared/fields/_number.html.erb +10 -0
- data/app/views/rsb/admin/shared/fields/_password.html.erb +10 -0
- data/app/views/rsb/admin/shared/fields/_select.html.erb +12 -0
- data/app/views/rsb/admin/shared/fields/_text.html.erb +10 -0
- data/app/views/rsb/admin/shared/fields/_textarea.html.erb +10 -0
- data/app/views/rsb/admin/shared/forbidden.html.erb +22 -0
- data/app/views/rsb/admin/themes/modern/views/shared/_header.html.erb +77 -0
- data/app/views/rsb/admin/themes/modern/views/shared/_sidebar.html.erb +135 -0
- data/app/views/rsb/admin/two_factor/backup_codes.html.erb +48 -0
- data/app/views/rsb/admin/two_factor/new.html.erb +53 -0
- data/config/locales/en.yml +140 -0
- data/config/locales/seo.en.yml +21 -0
- data/config/routes.rb +59 -0
- data/db/migrate/20260208000003_create_rsb_admin_tables.rb +43 -0
- data/db/migrate/20260214000001_add_otp_fields_to_rsb_admin_admin_users.rb +9 -0
- data/lib/generators/rsb/admin/install/install_generator.rb +45 -0
- data/lib/generators/rsb/admin/install/templates/rsb_admin_seeds.rb +24 -0
- data/lib/generators/rsb/admin/theme/templates/theme.css.tt +66 -0
- data/lib/generators/rsb/admin/theme/theme_generator.rb +218 -0
- data/lib/generators/rsb/admin/views/views_generator.rb +262 -0
- data/lib/rsb/admin/breadcrumb_item.rb +26 -0
- data/lib/rsb/admin/category_registration.rb +177 -0
- data/lib/rsb/admin/column_definition.rb +89 -0
- data/lib/rsb/admin/configuration.rb +69 -0
- data/lib/rsb/admin/engine.rb +34 -0
- data/lib/rsb/admin/filter_definition.rb +129 -0
- data/lib/rsb/admin/form_field_definition.rb +96 -0
- data/lib/rsb/admin/icons.rb +95 -0
- data/lib/rsb/admin/page_registration.rb +140 -0
- data/lib/rsb/admin/registry.rb +109 -0
- data/lib/rsb/admin/resource_dsl_context.rb +139 -0
- data/lib/rsb/admin/resource_registration.rb +287 -0
- data/lib/rsb/admin/settings_schema.rb +60 -0
- data/lib/rsb/admin/test_kit/helpers.rb +316 -0
- data/lib/rsb/admin/test_kit/resource_test_case.rb +193 -0
- data/lib/rsb/admin/test_kit.rb +11 -0
- data/lib/rsb/admin/theme_definition.rb +46 -0
- data/lib/rsb/admin/themes/modern.rb +44 -0
- data/lib/rsb/admin/version.rb +9 -0
- data/lib/rsb/admin.rb +177 -0
- data/lib/tasks/rsb/admin_tasks.rake +23 -0
- metadata +227 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RSB
|
|
4
|
+
module Admin
|
|
5
|
+
# Rails generator for scaffolding custom admin themes.
|
|
6
|
+
#
|
|
7
|
+
# This generator creates either a host-app theme (default) or a complete
|
|
8
|
+
# Rails engine gem for distributable themes. Both modes include:
|
|
9
|
+
# - CSS with all required `--rsb-admin-*` variables
|
|
10
|
+
# - View overrides (sidebar and header)
|
|
11
|
+
# - Theme registration code
|
|
12
|
+
#
|
|
13
|
+
# @example Generate a host-app theme
|
|
14
|
+
# rails generate rsb:admin:theme corporate
|
|
15
|
+
#
|
|
16
|
+
# @example Generate a theme as a Rails engine gem
|
|
17
|
+
# rails generate rsb:admin:theme corporate --engine
|
|
18
|
+
class ThemeGenerator < Rails::Generators::NamedBase
|
|
19
|
+
namespace 'rsb:admin:theme'
|
|
20
|
+
source_root File.expand_path('templates', __dir__)
|
|
21
|
+
|
|
22
|
+
desc 'Scaffold a new RSB Admin theme.'
|
|
23
|
+
|
|
24
|
+
class_option :engine, type: :boolean, default: false,
|
|
25
|
+
desc: 'Generate as a Rails engine gem (for distribution)'
|
|
26
|
+
|
|
27
|
+
# Creates the theme scaffold based on the `--engine` option.
|
|
28
|
+
#
|
|
29
|
+
# Dispatches to either {#create_host_app_scaffold} for host-app themes
|
|
30
|
+
# or {#create_engine_scaffold} for engine gems.
|
|
31
|
+
#
|
|
32
|
+
# @return [void]
|
|
33
|
+
def create_theme
|
|
34
|
+
if options[:engine]
|
|
35
|
+
create_engine_scaffold
|
|
36
|
+
else
|
|
37
|
+
create_host_app_scaffold
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Prints post-generation instructions to the console.
|
|
42
|
+
#
|
|
43
|
+
# Instructions differ based on whether a host-app or engine scaffold
|
|
44
|
+
# was generated.
|
|
45
|
+
#
|
|
46
|
+
# @return [void]
|
|
47
|
+
def print_instructions
|
|
48
|
+
say ''
|
|
49
|
+
if options[:engine]
|
|
50
|
+
say "Theme engine scaffold created at #{engine_dir}/", :green
|
|
51
|
+
say ''
|
|
52
|
+
say 'Next steps:'
|
|
53
|
+
say " 1. cd #{engine_dir}"
|
|
54
|
+
say " 2. Customize the CSS variables in app/assets/stylesheets/rsb/admin/themes/#{file_name}.css"
|
|
55
|
+
say " 3. Customize views in app/views/rsb/admin/themes/#{file_name}/views/"
|
|
56
|
+
say ' 4. Build: bundle exec rake build'
|
|
57
|
+
say " 5. Add to host app Gemfile: gem 'rsb-admin-#{file_name}-theme', path: '#{engine_dir}'"
|
|
58
|
+
else
|
|
59
|
+
say 'Theme scaffold created!', :green
|
|
60
|
+
say ''
|
|
61
|
+
say 'Next steps:'
|
|
62
|
+
say " 1. Customize CSS variables in app/assets/stylesheets/admin/themes/#{file_name}.css"
|
|
63
|
+
say " 2. Customize views in app/views/admin/themes/#{file_name}/views/"
|
|
64
|
+
say " 3. Activate via Settings page or: RSB::Admin.configure { |c| c.theme = :#{file_name} }"
|
|
65
|
+
end
|
|
66
|
+
say ''
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
# ── Host App Scaffold ──────────────────────────
|
|
72
|
+
|
|
73
|
+
# Creates a host-app theme scaffold with CSS, views, and an initializer.
|
|
74
|
+
#
|
|
75
|
+
# Files created:
|
|
76
|
+
# - `app/assets/stylesheets/admin/themes/{name}.css`
|
|
77
|
+
# - `app/views/admin/themes/{name}/views/shared/_sidebar.html.erb`
|
|
78
|
+
# - `app/views/admin/themes/{name}/views/shared/_header.html.erb`
|
|
79
|
+
# - `config/initializers/rsb_admin_{name}_theme.rb`
|
|
80
|
+
#
|
|
81
|
+
# @return [void]
|
|
82
|
+
# @api private
|
|
83
|
+
def create_host_app_scaffold
|
|
84
|
+
# CSS with all variables
|
|
85
|
+
template 'theme.css.tt',
|
|
86
|
+
"app/assets/stylesheets/admin/themes/#{file_name}.css"
|
|
87
|
+
|
|
88
|
+
# Copy default sidebar and header as starting points
|
|
89
|
+
copy_engine_view('shared/_sidebar.html.erb',
|
|
90
|
+
"app/views/admin/themes/#{file_name}/views/shared/_sidebar.html.erb")
|
|
91
|
+
copy_engine_view('shared/_header.html.erb',
|
|
92
|
+
"app/views/admin/themes/#{file_name}/views/shared/_header.html.erb")
|
|
93
|
+
|
|
94
|
+
# Initializer to register the theme
|
|
95
|
+
create_file "config/initializers/rsb_admin_#{file_name}_theme.rb", <<~RUBY
|
|
96
|
+
RSB::Admin.register_theme :#{file_name},
|
|
97
|
+
label: "#{class_name}",
|
|
98
|
+
css: "admin/themes/#{file_name}",
|
|
99
|
+
views_path: "admin/themes/#{file_name}/views"
|
|
100
|
+
RUBY
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# ── Engine Scaffold ────────────────────────────
|
|
104
|
+
|
|
105
|
+
# Creates a complete Rails engine gem scaffold for the theme.
|
|
106
|
+
#
|
|
107
|
+
# Creates a directory structure with:
|
|
108
|
+
# - Gemspec with `rsb-admin` dependency
|
|
109
|
+
# - Engine class with theme registration
|
|
110
|
+
# - CSS and view assets
|
|
111
|
+
# - Gemfile and Rakefile
|
|
112
|
+
#
|
|
113
|
+
# @return [void]
|
|
114
|
+
# @api private
|
|
115
|
+
def create_engine_scaffold
|
|
116
|
+
# Gemspec
|
|
117
|
+
create_file "#{engine_dir}/rsb-admin-#{file_name}-theme.gemspec", <<~GEMSPEC
|
|
118
|
+
Gem::Specification.new do |s|
|
|
119
|
+
s.name = "rsb-admin-#{file_name}-theme"
|
|
120
|
+
s.version = "0.1.0"
|
|
121
|
+
s.summary = "#{class_name} theme for RSB Admin"
|
|
122
|
+
s.description = "A custom theme for the RSB Admin panel."
|
|
123
|
+
s.license = "MIT"
|
|
124
|
+
s.authors = ["TODO: Your name"]
|
|
125
|
+
|
|
126
|
+
s.files = Dir["{app,lib}/**/*", "LICENSE", "README.md"]
|
|
127
|
+
s.require_paths = ["lib"]
|
|
128
|
+
|
|
129
|
+
s.add_dependency "rsb-admin"
|
|
130
|
+
end
|
|
131
|
+
GEMSPEC
|
|
132
|
+
|
|
133
|
+
# Main lib file
|
|
134
|
+
create_file "#{engine_dir}/lib/rsb/admin/#{file_name}_theme.rb", <<~RUBY
|
|
135
|
+
require "rsb/admin/#{file_name}_theme/engine"
|
|
136
|
+
RUBY
|
|
137
|
+
|
|
138
|
+
# Engine
|
|
139
|
+
create_file "#{engine_dir}/lib/rsb/admin/#{file_name}_theme/engine.rb", <<~RUBY
|
|
140
|
+
module RSB
|
|
141
|
+
module Admin
|
|
142
|
+
module #{class_name}Theme
|
|
143
|
+
class Engine < ::Rails::Engine
|
|
144
|
+
isolate_namespace RSB::Admin::#{class_name}Theme
|
|
145
|
+
|
|
146
|
+
initializer "rsb_admin_#{file_name}_theme.register" do
|
|
147
|
+
RSB::Admin.register_theme :#{file_name},
|
|
148
|
+
label: "#{class_name}",
|
|
149
|
+
css: "rsb/admin/themes/#{file_name}",
|
|
150
|
+
views_path: "rsb/admin/themes/#{file_name}/views"
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
RUBY
|
|
157
|
+
|
|
158
|
+
# CSS with all variables
|
|
159
|
+
template 'theme.css.tt',
|
|
160
|
+
"#{engine_dir}/app/assets/stylesheets/rsb/admin/themes/#{file_name}.css"
|
|
161
|
+
|
|
162
|
+
# Copy default sidebar and header as starting points
|
|
163
|
+
copy_engine_view('shared/_sidebar.html.erb',
|
|
164
|
+
"#{engine_dir}/app/views/rsb/admin/themes/#{file_name}/views/shared/_sidebar.html.erb")
|
|
165
|
+
copy_engine_view('shared/_header.html.erb',
|
|
166
|
+
"#{engine_dir}/app/views/rsb/admin/themes/#{file_name}/views/shared/_header.html.erb")
|
|
167
|
+
|
|
168
|
+
# Gemfile
|
|
169
|
+
create_file "#{engine_dir}/Gemfile", <<~GEMFILE
|
|
170
|
+
source "https://rubygems.org"
|
|
171
|
+
|
|
172
|
+
gemspec
|
|
173
|
+
|
|
174
|
+
gem "rsb-admin", path: "../../"
|
|
175
|
+
GEMFILE
|
|
176
|
+
|
|
177
|
+
# Rakefile
|
|
178
|
+
create_file "#{engine_dir}/Rakefile", <<~RAKE
|
|
179
|
+
require "bundler/gem_tasks"
|
|
180
|
+
RAKE
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Returns the directory name for the engine gem.
|
|
184
|
+
#
|
|
185
|
+
# @return [String] the engine directory name
|
|
186
|
+
# @api private
|
|
187
|
+
def engine_dir
|
|
188
|
+
"rsb-admin-#{file_name}-theme"
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Returns the theme name (same as file_name).
|
|
192
|
+
#
|
|
193
|
+
# @return [String] the theme name
|
|
194
|
+
# @api private
|
|
195
|
+
def theme_name
|
|
196
|
+
file_name
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Copies a view file from the RSB Admin engine to a destination path.
|
|
200
|
+
#
|
|
201
|
+
# If the source view doesn't exist in the engine, prints a skip message
|
|
202
|
+
# instead of failing.
|
|
203
|
+
#
|
|
204
|
+
# @param relative_path [String] the view path relative to `rsb/admin/`
|
|
205
|
+
# @param destination [String] the destination path for the copied view
|
|
206
|
+
# @return [void]
|
|
207
|
+
# @api private
|
|
208
|
+
def copy_engine_view(relative_path, destination)
|
|
209
|
+
source = RSB::Admin::Engine.root.join('app', 'views', 'rsb', 'admin', relative_path)
|
|
210
|
+
if File.exist?(source)
|
|
211
|
+
create_file destination, File.read(source)
|
|
212
|
+
else
|
|
213
|
+
say_status :skip, "#{relative_path} (source not found)", :yellow
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RSB
|
|
4
|
+
module Admin
|
|
5
|
+
# Generator to export RSB Admin views to the host application for customization.
|
|
6
|
+
#
|
|
7
|
+
# This generator copies view files from the RSB Admin engine (or a specific theme)
|
|
8
|
+
# to the host application's view override path. It supports selective export via
|
|
9
|
+
# `--only` group filtering and theme-specific views via `--theme`.
|
|
10
|
+
#
|
|
11
|
+
# @example Export all views
|
|
12
|
+
# rails generate rsb:admin:views
|
|
13
|
+
#
|
|
14
|
+
# @example Export only sidebar and header
|
|
15
|
+
# rails generate rsb:admin:views --only sidebar,header
|
|
16
|
+
#
|
|
17
|
+
# @example Export modern theme views
|
|
18
|
+
# rails generate rsb:admin:views --theme modern
|
|
19
|
+
#
|
|
20
|
+
# @example Force overwrite existing files
|
|
21
|
+
# rails generate rsb:admin:views --force
|
|
22
|
+
class ViewsGenerator < Rails::Generators::Base
|
|
23
|
+
namespace 'rsb:admin:views'
|
|
24
|
+
desc 'Export RSB Admin views to your application for customization.'
|
|
25
|
+
|
|
26
|
+
class_option :only, type: :string, default: nil,
|
|
27
|
+
desc: 'Comma-separated list of view groups to export (sidebar,header,layout,breadcrumbs,resources,fields,dashboard,sessions)'
|
|
28
|
+
class_option :theme, type: :string, default: nil,
|
|
29
|
+
desc: 'Export views for a specific theme (e.g., modern)'
|
|
30
|
+
class_option :force, type: :boolean, default: false,
|
|
31
|
+
desc: 'Overwrite existing files'
|
|
32
|
+
|
|
33
|
+
# Mapping of view groups to their file paths (relative to the views root).
|
|
34
|
+
# Paths use the partial naming convention (with underscore prefix for partials).
|
|
35
|
+
VIEW_GROUPS = {
|
|
36
|
+
'sidebar' => ['shared/_sidebar.html.erb'],
|
|
37
|
+
'header' => ['shared/_header.html.erb'],
|
|
38
|
+
'layout' => ['layouts/rsb/admin/application.html.erb'],
|
|
39
|
+
'breadcrumbs' => ['shared/_breadcrumbs.html.erb'],
|
|
40
|
+
'resources' => [
|
|
41
|
+
'resources/index.html.erb',
|
|
42
|
+
'resources/show.html.erb',
|
|
43
|
+
'resources/new.html.erb',
|
|
44
|
+
'resources/edit.html.erb',
|
|
45
|
+
'resources/_table.html.erb',
|
|
46
|
+
'resources/_filters.html.erb',
|
|
47
|
+
'resources/_form.html.erb',
|
|
48
|
+
'resources/_pagination.html.erb'
|
|
49
|
+
],
|
|
50
|
+
'fields' => [
|
|
51
|
+
'shared/fields/_text.html.erb',
|
|
52
|
+
'shared/fields/_textarea.html.erb',
|
|
53
|
+
'shared/fields/_select.html.erb',
|
|
54
|
+
'shared/fields/_checkbox.html.erb',
|
|
55
|
+
'shared/fields/_number.html.erb',
|
|
56
|
+
'shared/fields/_email.html.erb',
|
|
57
|
+
'shared/fields/_password.html.erb',
|
|
58
|
+
'shared/fields/_datetime.html.erb',
|
|
59
|
+
'shared/fields/_json.html.erb'
|
|
60
|
+
],
|
|
61
|
+
'dashboard' => ['dashboard/index.html.erb'],
|
|
62
|
+
'sessions' => ['sessions/new.html.erb']
|
|
63
|
+
}.freeze
|
|
64
|
+
|
|
65
|
+
# Copies the selected view files from the engine to the host application.
|
|
66
|
+
#
|
|
67
|
+
# This is the main action that exports views, handling group filtering,
|
|
68
|
+
# theme selection, and skip/force logic for existing files.
|
|
69
|
+
#
|
|
70
|
+
# @return [void]
|
|
71
|
+
def copy_views
|
|
72
|
+
groups = selected_groups
|
|
73
|
+
source_root = resolve_source_root
|
|
74
|
+
|
|
75
|
+
unless File.directory?(source_root)
|
|
76
|
+
say_status :error, "Source directory not found: #{source_root}", :red
|
|
77
|
+
return
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
files_copied = 0
|
|
81
|
+
files_skipped = 0
|
|
82
|
+
|
|
83
|
+
groups.each_value do |paths|
|
|
84
|
+
paths.each do |relative_path|
|
|
85
|
+
source = source_path_for(relative_path)
|
|
86
|
+
destination = File.join(target_directory, relative_path)
|
|
87
|
+
|
|
88
|
+
unless File.exist?(source)
|
|
89
|
+
say_status :skip, "#{relative_path} (not found in source)", :yellow
|
|
90
|
+
next
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
if File.exist?(destination) && !options[:force]
|
|
94
|
+
say_status :skip, "#{relative_path} (already exists, use --force to overwrite)", :yellow
|
|
95
|
+
files_skipped += 1
|
|
96
|
+
else
|
|
97
|
+
copy_file_with_status(source, destination)
|
|
98
|
+
files_copied += 1
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
say ''
|
|
104
|
+
say "Exported #{files_copied} view(s) to #{target_directory}/", :green
|
|
105
|
+
say "Skipped #{files_skipped} existing file(s)." if files_skipped.positive?
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Sets up the view_overrides_path configuration if not already configured.
|
|
109
|
+
#
|
|
110
|
+
# Creates an initializer that configures the override path when the
|
|
111
|
+
# configuration is currently nil. This ensures views exported to the
|
|
112
|
+
# host app will be found by the engine.
|
|
113
|
+
#
|
|
114
|
+
# @return [void]
|
|
115
|
+
def setup_override_path
|
|
116
|
+
return if RSB::Admin.configuration.view_overrides_path.present?
|
|
117
|
+
|
|
118
|
+
override_path = 'rsb_admin_overrides'
|
|
119
|
+
initializer_path = 'config/initializers/rsb_admin_views.rb'
|
|
120
|
+
|
|
121
|
+
if File.exist?(File.join(destination_root, initializer_path))
|
|
122
|
+
say_status :skip, initializer_path, :yellow
|
|
123
|
+
return
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
create_file initializer_path, <<~RUBY
|
|
127
|
+
RSB::Admin.configure do |config|
|
|
128
|
+
config.view_overrides_path = "#{override_path}"
|
|
129
|
+
end
|
|
130
|
+
RUBY
|
|
131
|
+
|
|
132
|
+
say ''
|
|
133
|
+
say "Created initializer to set view_overrides_path = \"#{override_path}\"", :green
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Prints instructions about the view override chain.
|
|
137
|
+
#
|
|
138
|
+
# Explains how the Rails view resolution works with the exported files,
|
|
139
|
+
# helping developers understand the priority order.
|
|
140
|
+
#
|
|
141
|
+
# @return [void]
|
|
142
|
+
def print_instructions
|
|
143
|
+
say ''
|
|
144
|
+
say 'View override chain (highest priority first):', :cyan
|
|
145
|
+
say " 1. app/views/#{override_path_name}/ (your customizations)"
|
|
146
|
+
say ' 2. Theme views (if active theme has view overrides)'
|
|
147
|
+
say ' 3. RSB Admin engine defaults (fallback)'
|
|
148
|
+
say ''
|
|
149
|
+
say 'Edit the exported files to customize your admin panel.'
|
|
150
|
+
say "Files you didn't export will continue using engine defaults."
|
|
151
|
+
say ''
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
private
|
|
155
|
+
|
|
156
|
+
# Returns the filtered set of view groups based on the `--only` option.
|
|
157
|
+
#
|
|
158
|
+
# If `--only` is not specified, returns all groups. If specified, validates
|
|
159
|
+
# group names and exits with an error message if any invalid groups are found.
|
|
160
|
+
#
|
|
161
|
+
# @return [Hash{String => Array<String>}] filtered view groups hash
|
|
162
|
+
# @raise [SystemExit] if invalid group names are provided
|
|
163
|
+
def selected_groups
|
|
164
|
+
if options[:only]
|
|
165
|
+
keys = options[:only].split(',').map(&:strip)
|
|
166
|
+
invalid = keys - VIEW_GROUPS.keys
|
|
167
|
+
if invalid.any?
|
|
168
|
+
say_status :error,
|
|
169
|
+
"Unknown view group(s): #{invalid.join(', ')}. Valid groups: #{VIEW_GROUPS.keys.join(', ')}", :red
|
|
170
|
+
exit 1
|
|
171
|
+
end
|
|
172
|
+
VIEW_GROUPS.slice(*keys)
|
|
173
|
+
else
|
|
174
|
+
VIEW_GROUPS
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Resolves the source root directory based on the `--theme` option.
|
|
179
|
+
#
|
|
180
|
+
# If a theme is specified, uses the theme's views_path if available,
|
|
181
|
+
# otherwise falls back to engine defaults. If no theme is specified,
|
|
182
|
+
# uses the engine's default view directory.
|
|
183
|
+
#
|
|
184
|
+
# @return [String] absolute path to the source view directory
|
|
185
|
+
# @raise [SystemExit] if an invalid theme name is provided
|
|
186
|
+
def resolve_source_root
|
|
187
|
+
if options[:theme]
|
|
188
|
+
theme_key = options[:theme].to_sym
|
|
189
|
+
theme = RSB::Admin.themes[theme_key]
|
|
190
|
+
if theme.nil?
|
|
191
|
+
say_status :error, "Unknown theme: #{options[:theme]}. Available: #{RSB::Admin.themes.keys.join(', ')}",
|
|
192
|
+
:red
|
|
193
|
+
exit 1
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
if theme.views_path
|
|
197
|
+
# Theme has custom views — use them as source
|
|
198
|
+
RSB::Admin::Engine.root.join('app', 'views', theme.views_path).to_s
|
|
199
|
+
else
|
|
200
|
+
# Theme has no custom views — fall back to engine defaults
|
|
201
|
+
engine_views_root
|
|
202
|
+
end
|
|
203
|
+
else
|
|
204
|
+
engine_views_root
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Returns the full source path for a given relative view path.
|
|
209
|
+
#
|
|
210
|
+
# Handles the special case of layout files, which live under `app/views/layouts/`
|
|
211
|
+
# instead of the namespaced view directory.
|
|
212
|
+
#
|
|
213
|
+
# @param relative_path [String] the relative path from VIEW_GROUPS
|
|
214
|
+
# @return [String] absolute path to the source file
|
|
215
|
+
def source_path_for(relative_path)
|
|
216
|
+
if relative_path.start_with?('layouts/')
|
|
217
|
+
RSB::Admin::Engine.root.join('app', 'views', relative_path).to_s
|
|
218
|
+
else
|
|
219
|
+
File.join(resolve_source_root, relative_path)
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Returns the engine's default namespaced views root directory.
|
|
224
|
+
#
|
|
225
|
+
# @return [String] absolute path to rsb/admin view directory in the engine
|
|
226
|
+
def engine_views_root
|
|
227
|
+
RSB::Admin::Engine.root.join('app', 'views', 'rsb', 'admin').to_s
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Returns the target directory where views will be exported.
|
|
231
|
+
#
|
|
232
|
+
# Uses the configured view_overrides_path or the default "rsb_admin_overrides".
|
|
233
|
+
#
|
|
234
|
+
# @return [String] absolute path to the target view directory
|
|
235
|
+
def target_directory
|
|
236
|
+
File.join(destination_root, 'app', 'views', override_path_name)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Returns the configured or default override path name.
|
|
240
|
+
#
|
|
241
|
+
# @return [String] the view override path name
|
|
242
|
+
def override_path_name
|
|
243
|
+
RSB::Admin.configuration.view_overrides_path || 'rsb_admin_overrides'
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Copies a file and prints a status message.
|
|
247
|
+
#
|
|
248
|
+
# Creates parent directories if needed and reports the relative path
|
|
249
|
+
# that was created.
|
|
250
|
+
#
|
|
251
|
+
# @param source [String] absolute path to source file
|
|
252
|
+
# @param destination [String] absolute path to destination file
|
|
253
|
+
# @return [void]
|
|
254
|
+
def copy_file_with_status(source, destination)
|
|
255
|
+
FileUtils.mkdir_p(File.dirname(destination))
|
|
256
|
+
FileUtils.cp(source, destination)
|
|
257
|
+
relative = destination.sub("#{destination_root}/", '')
|
|
258
|
+
say_status :create, relative, :green
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RSB
|
|
4
|
+
module Admin
|
|
5
|
+
# Represents a breadcrumb item in admin navigation.
|
|
6
|
+
#
|
|
7
|
+
# BreadcrumbItem is an immutable data structure representing a single item
|
|
8
|
+
# in a breadcrumb trail. The path is nil for the current (last) item.
|
|
9
|
+
#
|
|
10
|
+
# @!attribute [r] label
|
|
11
|
+
# @return [String] the text to display for this breadcrumb
|
|
12
|
+
# @!attribute [r] path
|
|
13
|
+
# @return [String, nil] the URL path (nil for the current/last item)
|
|
14
|
+
#
|
|
15
|
+
# @example Building a breadcrumb trail
|
|
16
|
+
# [
|
|
17
|
+
# BreadcrumbItem.new(label: "Admin", path: "/admin"),
|
|
18
|
+
# BreadcrumbItem.new(label: "Users", path: "/admin/users"),
|
|
19
|
+
# BreadcrumbItem.new(label: "John Doe", path: nil) # current page
|
|
20
|
+
# ]
|
|
21
|
+
BreadcrumbItem = Data.define(
|
|
22
|
+
:label, # String
|
|
23
|
+
:path # String | nil — nil for current (last) item
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RSB
|
|
4
|
+
module Admin
|
|
5
|
+
# Represents a category of admin resources and pages.
|
|
6
|
+
#
|
|
7
|
+
# Categories group related resources and pages in the admin interface sidebar.
|
|
8
|
+
# Each category has a name and contains collections of resources (CRUD interfaces
|
|
9
|
+
# for models) and pages (custom non-CRUD interfaces).
|
|
10
|
+
#
|
|
11
|
+
# @!attribute [r] name
|
|
12
|
+
# @return [String] the category name
|
|
13
|
+
#
|
|
14
|
+
# @!attribute [r] resources
|
|
15
|
+
# @return [Array<ResourceRegistration>] registered resources in this category
|
|
16
|
+
#
|
|
17
|
+
# @!attribute [r] pages
|
|
18
|
+
# @return [Array<PageRegistration>] registered pages in this category
|
|
19
|
+
#
|
|
20
|
+
# @example Creating a category with resources and pages
|
|
21
|
+
# category = CategoryRegistration.new("Users")
|
|
22
|
+
# category.resource User, icon: "users", actions: [:index, :show]
|
|
23
|
+
# category.page :dashboard, label: "Dashboard", controller: "admin/dashboard"
|
|
24
|
+
#
|
|
25
|
+
# @see Registry#register_category
|
|
26
|
+
# @see ResourceRegistration
|
|
27
|
+
# @see PageRegistration
|
|
28
|
+
class CategoryRegistration
|
|
29
|
+
attr_reader :name, :resources, :pages
|
|
30
|
+
|
|
31
|
+
# Initialize a new category with an empty set of resources and pages.
|
|
32
|
+
#
|
|
33
|
+
# @param name [String] the category name
|
|
34
|
+
#
|
|
35
|
+
# @example
|
|
36
|
+
# category = CategoryRegistration.new("Authentication")
|
|
37
|
+
def initialize(name)
|
|
38
|
+
@name = name
|
|
39
|
+
@resources = []
|
|
40
|
+
@pages = []
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Register a resource (ActiveRecord model) in this category.
|
|
44
|
+
#
|
|
45
|
+
# Resources can be registered with or without a configuration block.
|
|
46
|
+
# When a block is provided, it runs in the context of {ResourceDSLContext}
|
|
47
|
+
# and allows you to define columns, filters, and form fields inline.
|
|
48
|
+
#
|
|
49
|
+
# @param model_class [Class] the ActiveRecord model class to register
|
|
50
|
+
# @param icon [String, nil] optional icon identifier for the resource
|
|
51
|
+
# @param label [String, nil] optional custom label (defaults to humanized plural model name)
|
|
52
|
+
# @param actions [Array<Symbol>] allowed actions for this resource (default: `[:index, :show]`)
|
|
53
|
+
# @param controller [String, nil] optional custom controller path for delegation
|
|
54
|
+
# @param per_page [Integer, nil] number of records per page for index
|
|
55
|
+
# @param default_sort [Hash, nil] default sort configuration (e.g., `{ column: :created_at, direction: :desc }`)
|
|
56
|
+
# @param search_fields [Array<Symbol>, nil] searchable field names
|
|
57
|
+
# @param options [Hash] additional options passed to the registration
|
|
58
|
+
# @yield [ResourceDSLContext] optional block for defining columns, filters, and form fields
|
|
59
|
+
#
|
|
60
|
+
# @return [void]
|
|
61
|
+
#
|
|
62
|
+
# @example Basic resource registration
|
|
63
|
+
# resource User, icon: "users", actions: [:index, :show]
|
|
64
|
+
#
|
|
65
|
+
# @example Resource with inline DSL
|
|
66
|
+
# resource User, icon: "users", actions: [:index, :show, :edit, :update] do
|
|
67
|
+
# column :id, link: true
|
|
68
|
+
# column :email, sortable: true
|
|
69
|
+
# filter :email, type: :text
|
|
70
|
+
# form_field :email, type: :email, required: true
|
|
71
|
+
# end
|
|
72
|
+
#
|
|
73
|
+
# @example Resource with custom controller
|
|
74
|
+
# resource Identity, controller: "admin/identities", actions: [:index, :show, :suspend]
|
|
75
|
+
#
|
|
76
|
+
# @see ResourceDSLContext
|
|
77
|
+
# @see ResourceRegistration
|
|
78
|
+
def resource(model_class, icon: nil, label: nil, actions: %i[index show], controller: nil,
|
|
79
|
+
per_page: nil, default_sort: nil, search_fields: nil,
|
|
80
|
+
**options, &block)
|
|
81
|
+
dsl = nil
|
|
82
|
+
if block
|
|
83
|
+
dsl = ResourceDSLContext.new
|
|
84
|
+
dsl.instance_eval(&block)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
@resources << ResourceRegistration.new(
|
|
88
|
+
model_class: model_class,
|
|
89
|
+
category_name: @name,
|
|
90
|
+
icon: icon,
|
|
91
|
+
label: label || model_class.model_name.human.pluralize,
|
|
92
|
+
actions: actions,
|
|
93
|
+
controller: controller,
|
|
94
|
+
columns: dsl&.columns&.presence,
|
|
95
|
+
filters: dsl&.filters&.presence,
|
|
96
|
+
form_fields: dsl&.form_fields&.presence,
|
|
97
|
+
per_page: per_page,
|
|
98
|
+
default_sort: default_sort,
|
|
99
|
+
search_fields: search_fields,
|
|
100
|
+
**options
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Register a custom page in this category.
|
|
105
|
+
#
|
|
106
|
+
# Pages are non-CRUD admin interfaces that require custom controllers.
|
|
107
|
+
# Unlike resources, pages don't map to a specific model.
|
|
108
|
+
#
|
|
109
|
+
# @param key [Symbol, String] unique identifier for this page
|
|
110
|
+
# @param label [String] the human-readable page name
|
|
111
|
+
# @param icon [String, nil] optional icon identifier
|
|
112
|
+
# @param controller [String] the controller path (e.g., "admin/dashboard")
|
|
113
|
+
# @param actions [Array<Hash>] custom action definitions with `:key`, `:label`, `:method`, `:confirm`
|
|
114
|
+
#
|
|
115
|
+
# @return [PageRegistration] the created page registration
|
|
116
|
+
#
|
|
117
|
+
# @example Basic page
|
|
118
|
+
# page :dashboard, label: "Dashboard", icon: "home", controller: "admin/dashboard"
|
|
119
|
+
#
|
|
120
|
+
# @example Page with custom actions
|
|
121
|
+
# page :usage, label: "Usage Reports", controller: "admin/usage",
|
|
122
|
+
# actions: [
|
|
123
|
+
# { key: :index, label: "Overview" },
|
|
124
|
+
# { key: :export, label: "Export CSV", method: :post, confirm: "Export data?" }
|
|
125
|
+
# ]
|
|
126
|
+
#
|
|
127
|
+
# @see PageRegistration
|
|
128
|
+
def page(key, label:, controller:, icon: nil, actions: [])
|
|
129
|
+
registration = PageRegistration.build(
|
|
130
|
+
key: key,
|
|
131
|
+
label: label,
|
|
132
|
+
icon: icon,
|
|
133
|
+
controller: controller,
|
|
134
|
+
category_name: @name,
|
|
135
|
+
actions: actions
|
|
136
|
+
)
|
|
137
|
+
@pages << registration
|
|
138
|
+
registration
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Find a resource registration by its model class.
|
|
142
|
+
#
|
|
143
|
+
# @param model_class [Class] the ActiveRecord model class to search for
|
|
144
|
+
#
|
|
145
|
+
# @return [ResourceRegistration, nil] the matching resource registration, or nil if not found
|
|
146
|
+
#
|
|
147
|
+
# @example
|
|
148
|
+
# category.find_resource(User) #=> ResourceRegistration instance or nil
|
|
149
|
+
def find_resource(model_class)
|
|
150
|
+
@resources.find { |r| r.model_class == model_class }
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Merge another category's resources and pages into this category.
|
|
154
|
+
#
|
|
155
|
+
# This is used when multiple gems or initializers register resources
|
|
156
|
+
# in the same category. All resources and pages are combined.
|
|
157
|
+
#
|
|
158
|
+
# @param other [CategoryRegistration] the category to merge from
|
|
159
|
+
#
|
|
160
|
+
# @return [void]
|
|
161
|
+
#
|
|
162
|
+
# @example
|
|
163
|
+
# category1 = CategoryRegistration.new("Auth")
|
|
164
|
+
# category1.resource User, actions: [:index]
|
|
165
|
+
#
|
|
166
|
+
# category2 = CategoryRegistration.new("Auth")
|
|
167
|
+
# category2.resource Session, actions: [:index]
|
|
168
|
+
#
|
|
169
|
+
# category1.merge(category2)
|
|
170
|
+
# category1.resources.size #=> 2
|
|
171
|
+
def merge(other)
|
|
172
|
+
@resources.concat(other.resources)
|
|
173
|
+
@pages.concat(other.pages)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|