admin_suite 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/.gitignore +10 -0
- data/Gemfile +13 -0
- data/LICENSE.txt +22 -0
- data/README.md +7 -0
- data/Rakefile +11 -0
- data/app/assets/admin_suite.css +444 -0
- data/app/assets/admin_suite_tailwind.css +8 -0
- data/app/assets/builds/admin_suite_tailwind.css +8 -0
- data/app/assets/rouge.css +218 -0
- data/app/assets/tailwind/admin_suite.css +22 -0
- data/app/controllers/admin_suite/application_controller.rb +118 -0
- data/app/controllers/admin_suite/dashboard_controller.rb +258 -0
- data/app/controllers/admin_suite/docs_controller.rb +155 -0
- data/app/controllers/admin_suite/portals_controller.rb +22 -0
- data/app/controllers/admin_suite/resources_controller.rb +238 -0
- data/app/helpers/admin_suite/base_helper.rb +1199 -0
- data/app/helpers/admin_suite/icon_helper.rb +61 -0
- data/app/helpers/admin_suite/panels_helper.rb +52 -0
- data/app/helpers/admin_suite/resources_helper.rb +15 -0
- data/app/helpers/admin_suite/theme_helper.rb +99 -0
- data/app/javascript/controllers/admin_suite/click_actions_controller.js +73 -0
- data/app/javascript/controllers/admin_suite/clipboard_controller.js +57 -0
- data/app/javascript/controllers/admin_suite/code_editor_controller.js +45 -0
- data/app/javascript/controllers/admin_suite/file_upload_controller.js +238 -0
- data/app/javascript/controllers/admin_suite/json_editor_controller.js +62 -0
- data/app/javascript/controllers/admin_suite/live_filter_controller.js +71 -0
- data/app/javascript/controllers/admin_suite/markdown_editor_controller.js +67 -0
- data/app/javascript/controllers/admin_suite/searchable_select_controller.js +171 -0
- data/app/javascript/controllers/admin_suite/sidebar_controller.js +33 -0
- data/app/javascript/controllers/admin_suite/tag_select_controller.js +193 -0
- data/app/javascript/controllers/admin_suite/toggle_switch_controller.js +66 -0
- data/app/views/admin_suite/dashboard/index.html.erb +21 -0
- data/app/views/admin_suite/docs/index.html.erb +86 -0
- data/app/views/admin_suite/panels/_cards.html.erb +107 -0
- data/app/views/admin_suite/panels/_chart.html.erb +47 -0
- data/app/views/admin_suite/panels/_health.html.erb +44 -0
- data/app/views/admin_suite/panels/_recent.html.erb +56 -0
- data/app/views/admin_suite/panels/_stat.html.erb +64 -0
- data/app/views/admin_suite/panels/_table.html.erb +36 -0
- data/app/views/admin_suite/portals/show.html.erb +75 -0
- data/app/views/admin_suite/resources/_form.html.erb +32 -0
- data/app/views/admin_suite/resources/edit.html.erb +24 -0
- data/app/views/admin_suite/resources/index.html.erb +315 -0
- data/app/views/admin_suite/resources/new.html.erb +22 -0
- data/app/views/admin_suite/resources/show.html.erb +184 -0
- data/app/views/admin_suite/shared/_flash.html.erb +30 -0
- data/app/views/admin_suite/shared/_form.html.erb +60 -0
- data/app/views/admin_suite/shared/_json_editor_field.html.erb +52 -0
- data/app/views/admin_suite/shared/_sidebar.html.erb +94 -0
- data/app/views/admin_suite/shared/_toggle_cell.html.erb +34 -0
- data/app/views/admin_suite/shared/_topbar.html.erb +47 -0
- data/app/views/layouts/admin_suite/application.html.erb +79 -0
- data/lib/admin/base/action_executor.rb +155 -0
- data/lib/admin/base/action_handler.rb +31 -0
- data/lib/admin/base/filter_builder.rb +121 -0
- data/lib/admin/base/resource.rb +541 -0
- data/lib/admin_suite/configuration.rb +42 -0
- data/lib/admin_suite/engine.rb +101 -0
- data/lib/admin_suite/markdown_renderer.rb +115 -0
- data/lib/admin_suite/portal_definition.rb +64 -0
- data/lib/admin_suite/portal_registry.rb +32 -0
- data/lib/admin_suite/theme_palette.rb +36 -0
- data/lib/admin_suite/ui/dashboard_definition.rb +69 -0
- data/lib/admin_suite/ui/field_renderer_registry.rb +119 -0
- data/lib/admin_suite/ui/form_field_renderer.rb +48 -0
- data/lib/admin_suite/ui/show_formatter_registry.rb +120 -0
- data/lib/admin_suite/ui/show_value_formatter.rb +70 -0
- data/lib/admin_suite/version.rb +10 -0
- data/lib/admin_suite.rb +54 -0
- data/lib/generators/admin_suite/install/install_generator.rb +23 -0
- data/lib/generators/admin_suite/install/templates/admin_suite.rb +60 -0
- data/lib/generators/admin_suite/resource/resource_generator.rb +83 -0
- data/lib/generators/admin_suite/resource/templates/resource.rb.tt +47 -0
- data/lib/generators/admin_suite/scaffold/scaffold_generator.rb +28 -0
- data/lib/tasks/admin_suite_tailwind.rake +28 -0
- data/lib/tasks/admin_suite_test.rake +11 -0
- data/test/dummy/Gemfile +21 -0
- data/test/dummy/README.md +24 -0
- data/test/dummy/Rakefile +6 -0
- data/test/dummy/app/assets/stylesheets/application.css +10 -0
- data/test/dummy/app/controllers/application_controller.rb +4 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/models/application_record.rb +2 -0
- data/test/dummy/app/views/layouts/application.html.erb +28 -0
- data/test/dummy/app/views/pwa/manifest.json.erb +22 -0
- data/test/dummy/app/views/pwa/service-worker.js +26 -0
- data/test/dummy/bin/ci +6 -0
- data/test/dummy/bin/dev +2 -0
- data/test/dummy/bin/rails +4 -0
- data/test/dummy/bin/rake +4 -0
- data/test/dummy/bin/setup +35 -0
- data/test/dummy/config/application.rb +43 -0
- data/test/dummy/config/boot.rb +3 -0
- data/test/dummy/config/ci.rb +19 -0
- data/test/dummy/config/database.yml +31 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +57 -0
- data/test/dummy/config/environments/production.rb +67 -0
- data/test/dummy/config/environments/test.rb +42 -0
- data/test/dummy/config/initializers/assets.rb +7 -0
- data/test/dummy/config/initializers/content_security_policy.rb +29 -0
- data/test/dummy/config/initializers/filter_parameter_logging.rb +8 -0
- data/test/dummy/config/initializers/inflections.rb +16 -0
- data/test/dummy/config/locales/en.yml +31 -0
- data/test/dummy/config/puma.rb +39 -0
- data/test/dummy/config/routes.rb +16 -0
- data/test/dummy/config.ru +6 -0
- data/test/dummy/db/seeds.rb +9 -0
- data/test/dummy/log/test.log +441 -0
- data/test/dummy/public/400.html +135 -0
- data/test/dummy/public/404.html +135 -0
- data/test/dummy/public/406-unsupported-browser.html +135 -0
- data/test/dummy/public/422.html +135 -0
- data/test/dummy/public/500.html +135 -0
- data/test/dummy/public/icon.png +0 -0
- data/test/dummy/public/icon.svg +3 -0
- data/test/dummy/public/robots.txt +1 -0
- data/test/dummy/test/test_helper.rb +15 -0
- data/test/dummy/tmp/local_secret.txt +1 -0
- data/test/fixtures/docs/progress/PROGRESS_REPORT.md +6 -0
- data/test/integration/dashboard_test.rb +13 -0
- data/test/integration/docs_test.rb +46 -0
- data/test/integration/theme_test.rb +27 -0
- data/test/lib/markdown_renderer_test.rb +20 -0
- data/test/lib/theme_palette_test.rb +24 -0
- data/test/test_helper.rb +11 -0
- metadata +264 -0
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Admin
|
|
4
|
+
module Base
|
|
5
|
+
# Base class for admin resource definitions
|
|
6
|
+
#
|
|
7
|
+
# Provides a declarative DSL for defining admin resources with:
|
|
8
|
+
# - Index configuration (columns, filters, search, sort, stats)
|
|
9
|
+
# - Form configuration (fields with various types)
|
|
10
|
+
# - Actions (single and bulk)
|
|
11
|
+
# - Show page sections
|
|
12
|
+
# - Export capabilities
|
|
13
|
+
#
|
|
14
|
+
# @example
|
|
15
|
+
# class Admin::Resources::CompanyResource < Admin::Base::Resource
|
|
16
|
+
# model Company
|
|
17
|
+
# portal :ops
|
|
18
|
+
# section :content
|
|
19
|
+
#
|
|
20
|
+
# index do
|
|
21
|
+
# searchable :name, :website
|
|
22
|
+
# sortable :name, :created_at, default: :name
|
|
23
|
+
#
|
|
24
|
+
# columns do
|
|
25
|
+
# column :name
|
|
26
|
+
# column :job_listings, -> (c) { c.job_listings.count }
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# filters do
|
|
30
|
+
# filter :search, type: :text
|
|
31
|
+
# filter :status, type: :select, options: %w[active inactive]
|
|
32
|
+
# end
|
|
33
|
+
# end
|
|
34
|
+
#
|
|
35
|
+
# form do
|
|
36
|
+
# field :name, required: true
|
|
37
|
+
# field :website, type: :url
|
|
38
|
+
# field :is_active, type: :toggle
|
|
39
|
+
# end
|
|
40
|
+
# end
|
|
41
|
+
class Resource
|
|
42
|
+
class << self
|
|
43
|
+
# Model configuration
|
|
44
|
+
attr_reader :model_class, :portal_name, :section_name, :nav_label, :nav_icon, :nav_order
|
|
45
|
+
|
|
46
|
+
# Index configuration
|
|
47
|
+
attr_reader :index_config
|
|
48
|
+
|
|
49
|
+
# Form configuration
|
|
50
|
+
attr_reader :form_config
|
|
51
|
+
|
|
52
|
+
# Show configuration
|
|
53
|
+
attr_reader :show_config
|
|
54
|
+
|
|
55
|
+
# Actions configuration
|
|
56
|
+
attr_reader :actions_config
|
|
57
|
+
|
|
58
|
+
# Export configuration
|
|
59
|
+
attr_reader :export_formats
|
|
60
|
+
|
|
61
|
+
# Sets the model class for this resource
|
|
62
|
+
#
|
|
63
|
+
# @param klass [Class] The ActiveRecord model class
|
|
64
|
+
# @return [void]
|
|
65
|
+
def model(klass)
|
|
66
|
+
@model_class = klass
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Sets the portal this resource belongs to
|
|
70
|
+
#
|
|
71
|
+
# @param name [Symbol] Portal name (:ops or :ai)
|
|
72
|
+
# @return [void]
|
|
73
|
+
def portal(name)
|
|
74
|
+
@portal_name = name
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Sets the section within the portal
|
|
78
|
+
#
|
|
79
|
+
# @param name [Symbol] Section name
|
|
80
|
+
# @return [void]
|
|
81
|
+
def section(name)
|
|
82
|
+
@section_name = name
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Navigation metadata for this resource.
|
|
86
|
+
#
|
|
87
|
+
# @param label [String, nil] override label used in nav
|
|
88
|
+
# @param icon [String, Symbol, nil] lucide icon name (or raw svg string)
|
|
89
|
+
# @param order [Integer, nil] sort order within section
|
|
90
|
+
# @return [void]
|
|
91
|
+
def nav(label: nil, icon: nil, order: nil)
|
|
92
|
+
@nav_label = label if label.present?
|
|
93
|
+
@nav_icon = icon if icon.present?
|
|
94
|
+
@nav_order = order unless order.nil?
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Convenience setter/getter for nav icon.
|
|
98
|
+
#
|
|
99
|
+
# @param name [String, Symbol, nil]
|
|
100
|
+
# @return [String, Symbol, nil]
|
|
101
|
+
def icon(name = nil)
|
|
102
|
+
@nav_icon = name if name.present?
|
|
103
|
+
@nav_icon
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Convenience setter/getter for nav label.
|
|
107
|
+
#
|
|
108
|
+
# @param name [String, nil]
|
|
109
|
+
# @return [String, nil]
|
|
110
|
+
def label(name = nil)
|
|
111
|
+
@nav_label = name if name.present?
|
|
112
|
+
@nav_label
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Convenience setter/getter for nav order.
|
|
116
|
+
#
|
|
117
|
+
# @param value [Integer, nil]
|
|
118
|
+
# @return [Integer, nil]
|
|
119
|
+
def order(value = nil)
|
|
120
|
+
@nav_order = value unless value.nil?
|
|
121
|
+
@nav_order
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Configures the index view
|
|
125
|
+
#
|
|
126
|
+
# @yield Block for index configuration
|
|
127
|
+
# @return [void]
|
|
128
|
+
def index(&block)
|
|
129
|
+
@index_config = IndexConfig.new
|
|
130
|
+
@index_config.instance_eval(&block) if block_given?
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Configures the form (new/edit)
|
|
134
|
+
#
|
|
135
|
+
# @yield Block for form configuration
|
|
136
|
+
# @return [void]
|
|
137
|
+
def form(&block)
|
|
138
|
+
@form_config = FormConfig.new
|
|
139
|
+
@form_config.instance_eval(&block) if block_given?
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Configures the show view
|
|
143
|
+
#
|
|
144
|
+
# @yield Block for show configuration
|
|
145
|
+
# @return [void]
|
|
146
|
+
def show(&block)
|
|
147
|
+
@show_config = ShowConfig.new
|
|
148
|
+
@show_config.instance_eval(&block) if block_given?
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Configures actions
|
|
152
|
+
#
|
|
153
|
+
# @yield Block for actions configuration
|
|
154
|
+
# @return [void]
|
|
155
|
+
def actions(&block)
|
|
156
|
+
@actions_config = ActionsConfig.new
|
|
157
|
+
@actions_config.instance_eval(&block) if block_given?
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Configures export formats
|
|
161
|
+
#
|
|
162
|
+
# @param formats [Array<Symbol>] Export formats (:json, :csv)
|
|
163
|
+
# @return [void]
|
|
164
|
+
def exportable(*formats)
|
|
165
|
+
@export_formats = formats
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Returns the resource name derived from class name
|
|
169
|
+
#
|
|
170
|
+
# @return [String]
|
|
171
|
+
def resource_name
|
|
172
|
+
name.demodulize.sub(/Resource$/, "").underscore
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Returns the plural resource name
|
|
176
|
+
#
|
|
177
|
+
# @return [String]
|
|
178
|
+
def resource_name_plural
|
|
179
|
+
resource_name.pluralize
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Returns the human-readable name
|
|
183
|
+
#
|
|
184
|
+
# @return [String]
|
|
185
|
+
def human_name
|
|
186
|
+
model_class&.model_name&.human || resource_name.humanize
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Returns the human-readable plural name
|
|
190
|
+
#
|
|
191
|
+
# @return [String]
|
|
192
|
+
def human_name_plural
|
|
193
|
+
model_class&.model_name&.human(count: 2) || resource_name.pluralize.humanize
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Returns all registered resources
|
|
197
|
+
#
|
|
198
|
+
# @return [Array<Class>]
|
|
199
|
+
def registered_resources
|
|
200
|
+
@registered_resources ||= []
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Clears the registry (useful for development reloads).
|
|
204
|
+
#
|
|
205
|
+
# @return [void]
|
|
206
|
+
def reset_registry!
|
|
207
|
+
@registered_resources = []
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Called when a subclass is created
|
|
211
|
+
def inherited(subclass)
|
|
212
|
+
super
|
|
213
|
+
return if subclass.name&.include?("Base")
|
|
214
|
+
|
|
215
|
+
existing_idx = registered_resources.index { |r| r.name == subclass.name }
|
|
216
|
+
if existing_idx
|
|
217
|
+
registered_resources[existing_idx] = subclass
|
|
218
|
+
else
|
|
219
|
+
registered_resources << subclass
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Returns resources for a specific portal
|
|
224
|
+
#
|
|
225
|
+
# @param portal [Symbol] Portal name
|
|
226
|
+
# @return [Array<Class>]
|
|
227
|
+
def resources_for_portal(portal)
|
|
228
|
+
registered_resources.select { |r| r.portal_name == portal }
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Returns resources for a specific section
|
|
232
|
+
#
|
|
233
|
+
# @param portal [Symbol] Portal name
|
|
234
|
+
# @param section [Symbol] Section name
|
|
235
|
+
# @return [Array<Class>]
|
|
236
|
+
def resources_for_section(portal, section)
|
|
237
|
+
registered_resources.select { |r| r.portal_name == portal && r.section_name == section }
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Index view configuration
|
|
242
|
+
class IndexConfig
|
|
243
|
+
attr_reader :searchable_fields, :sortable_fields, :default_sort, :default_sort_direction,
|
|
244
|
+
:columns_list, :filters_list, :stats_list, :per_page
|
|
245
|
+
|
|
246
|
+
def initialize
|
|
247
|
+
@searchable_fields = []
|
|
248
|
+
@sortable_fields = []
|
|
249
|
+
@default_sort = nil
|
|
250
|
+
@default_sort_direction = :desc
|
|
251
|
+
@columns_list = []
|
|
252
|
+
@filters_list = []
|
|
253
|
+
@stats_list = []
|
|
254
|
+
@per_page = 25
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def searchable(*fields)
|
|
258
|
+
@searchable_fields = fields
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def sortable(*fields, default: nil, direction: :desc)
|
|
262
|
+
@sortable_fields = fields if fields.any?
|
|
263
|
+
@default_sort = default || fields.first
|
|
264
|
+
@default_sort_direction = direction
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def paginate(count)
|
|
268
|
+
@per_page = count
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def columns(&block)
|
|
272
|
+
builder = ColumnsBuilder.new
|
|
273
|
+
builder.instance_eval(&block) if block_given?
|
|
274
|
+
@columns_list = builder.columns
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def filters(&block)
|
|
278
|
+
builder = FiltersBuilder.new
|
|
279
|
+
builder.instance_eval(&block) if block_given?
|
|
280
|
+
@filters_list = builder.filters
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def stats(&block)
|
|
284
|
+
builder = StatsBuilder.new
|
|
285
|
+
builder.instance_eval(&block) if block_given?
|
|
286
|
+
@stats_list = builder.stats
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
class ColumnsBuilder
|
|
291
|
+
attr_reader :columns
|
|
292
|
+
|
|
293
|
+
def initialize
|
|
294
|
+
@columns = []
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def column(name, content = nil, **options)
|
|
298
|
+
@columns << ColumnDefinition.new(
|
|
299
|
+
name: name,
|
|
300
|
+
content: content,
|
|
301
|
+
render: options[:render],
|
|
302
|
+
header: options[:header] || name.to_s.humanize,
|
|
303
|
+
css_class: options[:class],
|
|
304
|
+
type: options[:type],
|
|
305
|
+
toggle_field: options[:toggle_field],
|
|
306
|
+
label_color: options[:label_color],
|
|
307
|
+
label_size: options[:label_size],
|
|
308
|
+
sortable: options[:sortable] || false
|
|
309
|
+
)
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
ColumnDefinition = Struct.new(:name, :content, :render, :header, :css_class, :type, :toggle_field, :label_color, :label_size, :sortable, keyword_init: true)
|
|
314
|
+
|
|
315
|
+
class FiltersBuilder
|
|
316
|
+
attr_reader :filters
|
|
317
|
+
|
|
318
|
+
def initialize
|
|
319
|
+
@filters = []
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def filter(name, **options)
|
|
323
|
+
select_options = options.key?(:options) ? options[:options] : options[:collection]
|
|
324
|
+
@filters << FilterDefinition.new(
|
|
325
|
+
name: name,
|
|
326
|
+
type: options[:type] || :text,
|
|
327
|
+
label: options[:label] || name.to_s.humanize,
|
|
328
|
+
placeholder: options[:placeholder],
|
|
329
|
+
options: select_options,
|
|
330
|
+
field: options[:field] || name,
|
|
331
|
+
apply: options[:apply]
|
|
332
|
+
)
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
FilterDefinition = Struct.new(:name, :type, :label, :placeholder, :options, :field, :apply, keyword_init: true)
|
|
337
|
+
|
|
338
|
+
class StatsBuilder
|
|
339
|
+
attr_reader :stats
|
|
340
|
+
|
|
341
|
+
def initialize
|
|
342
|
+
@stats = []
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def stat(name, calculator, **options)
|
|
346
|
+
@stats << StatDefinition.new(
|
|
347
|
+
name: name,
|
|
348
|
+
calculator: calculator,
|
|
349
|
+
color: options[:color]
|
|
350
|
+
)
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
StatDefinition = Struct.new(:name, :calculator, :color, keyword_init: true)
|
|
355
|
+
|
|
356
|
+
class FormConfig
|
|
357
|
+
attr_reader :fields_list
|
|
358
|
+
|
|
359
|
+
def initialize
|
|
360
|
+
@fields_list = []
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def field(name, **options)
|
|
364
|
+
@fields_list << FieldDefinition.new(
|
|
365
|
+
name: name,
|
|
366
|
+
type: options[:type] || :text,
|
|
367
|
+
required: options[:required] || false,
|
|
368
|
+
label: options[:label] || name.to_s.humanize,
|
|
369
|
+
help: options[:help],
|
|
370
|
+
placeholder: options[:placeholder],
|
|
371
|
+
collection: options[:collection],
|
|
372
|
+
create_url: options[:create_url],
|
|
373
|
+
accept: options[:accept],
|
|
374
|
+
rows: options[:rows],
|
|
375
|
+
readonly: options[:readonly] || false,
|
|
376
|
+
if_condition: options[:if],
|
|
377
|
+
unless_condition: options[:unless],
|
|
378
|
+
multiple: options[:multiple] || false,
|
|
379
|
+
creatable: options[:creatable] || false,
|
|
380
|
+
preview: options[:preview] != false,
|
|
381
|
+
variants: options[:variants],
|
|
382
|
+
label_color: options[:label_color],
|
|
383
|
+
label_size: options[:label_size]
|
|
384
|
+
)
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def section(title, **options, &block)
|
|
388
|
+
@fields_list << SectionDefinition.new(
|
|
389
|
+
title: title,
|
|
390
|
+
description: options[:description],
|
|
391
|
+
collapsible: options[:collapsible] || false,
|
|
392
|
+
collapsed: options[:collapsed] || false
|
|
393
|
+
)
|
|
394
|
+
instance_eval(&block) if block_given?
|
|
395
|
+
@fields_list << SectionEnd.new
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def row(**options, &block)
|
|
399
|
+
@fields_list << RowDefinition.new(cols: options[:cols] || 2)
|
|
400
|
+
instance_eval(&block) if block_given?
|
|
401
|
+
@fields_list << RowEnd.new
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
FieldDefinition = Struct.new(
|
|
406
|
+
:name, :type, :required, :label, :help, :placeholder,
|
|
407
|
+
:collection, :create_url, :accept, :rows, :readonly,
|
|
408
|
+
:if_condition, :unless_condition, :multiple, :creatable,
|
|
409
|
+
:preview, :variants, :label_color, :label_size,
|
|
410
|
+
keyword_init: true
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
SectionDefinition = Struct.new(:title, :description, :collapsible, :collapsed, keyword_init: true)
|
|
414
|
+
SectionEnd = Class.new
|
|
415
|
+
|
|
416
|
+
RowDefinition = Struct.new(:cols, keyword_init: true)
|
|
417
|
+
RowEnd = Class.new
|
|
418
|
+
|
|
419
|
+
class ShowConfig
|
|
420
|
+
attr_reader :sidebar_sections, :main_sections
|
|
421
|
+
|
|
422
|
+
def initialize
|
|
423
|
+
@sidebar_sections = []
|
|
424
|
+
@main_sections = []
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
def section(name, **options)
|
|
428
|
+
@main_sections << build_section(name, options)
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
def sidebar(&block)
|
|
432
|
+
@current_target = :sidebar
|
|
433
|
+
instance_eval(&block) if block_given?
|
|
434
|
+
@current_target = nil
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
def main(&block)
|
|
438
|
+
@current_target = :main
|
|
439
|
+
instance_eval(&block) if block_given?
|
|
440
|
+
@current_target = nil
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
def panel(name, **options)
|
|
444
|
+
section_def = build_section(name, options)
|
|
445
|
+
|
|
446
|
+
case @current_target
|
|
447
|
+
when :sidebar
|
|
448
|
+
@sidebar_sections << section_def
|
|
449
|
+
else
|
|
450
|
+
@main_sections << section_def
|
|
451
|
+
end
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
def sections_list
|
|
455
|
+
@main_sections
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
private
|
|
459
|
+
|
|
460
|
+
def build_section(name, options)
|
|
461
|
+
ShowSectionDefinition.new(
|
|
462
|
+
name: name,
|
|
463
|
+
fields: options[:fields] || [],
|
|
464
|
+
association: options[:association],
|
|
465
|
+
limit: options[:limit],
|
|
466
|
+
render: options[:render],
|
|
467
|
+
title: options[:title] || name.to_s.humanize,
|
|
468
|
+
display: options[:display] || :list,
|
|
469
|
+
columns: options[:columns] || [],
|
|
470
|
+
link_to: options[:link_to],
|
|
471
|
+
resource: options[:resource],
|
|
472
|
+
paginate: options[:paginate] || options[:pagination] || false,
|
|
473
|
+
per_page: options[:per_page],
|
|
474
|
+
collapsible: options[:collapsible] || false,
|
|
475
|
+
collapsed: options[:collapsed] || false
|
|
476
|
+
)
|
|
477
|
+
end
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
ShowSectionDefinition = Struct.new(
|
|
481
|
+
:name, :fields, :association, :limit, :render, :title,
|
|
482
|
+
:display, :columns, :link_to, :resource, :paginate, :per_page, :collapsible, :collapsed,
|
|
483
|
+
keyword_init: true
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
class ActionsConfig
|
|
487
|
+
attr_reader :member_actions, :collection_actions, :bulk_actions
|
|
488
|
+
|
|
489
|
+
def initialize
|
|
490
|
+
@member_actions = []
|
|
491
|
+
@collection_actions = []
|
|
492
|
+
@bulk_actions = []
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
def action(name, **options)
|
|
496
|
+
@member_actions << ActionDefinition.new(
|
|
497
|
+
name: name,
|
|
498
|
+
method: options[:method] || :post,
|
|
499
|
+
confirm: options[:confirm],
|
|
500
|
+
type: options[:type] || :button,
|
|
501
|
+
label: options[:label] || name.to_s.humanize,
|
|
502
|
+
icon: options[:icon],
|
|
503
|
+
color: options[:color],
|
|
504
|
+
if_condition: options[:if],
|
|
505
|
+
unless_condition: options[:unless]
|
|
506
|
+
)
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
def collection_action(name, **options)
|
|
510
|
+
@collection_actions << ActionDefinition.new(
|
|
511
|
+
name: name,
|
|
512
|
+
method: options[:method] || :post,
|
|
513
|
+
confirm: options[:confirm],
|
|
514
|
+
type: options[:type] || :button,
|
|
515
|
+
label: options[:label] || name.to_s.humanize,
|
|
516
|
+
icon: options[:icon],
|
|
517
|
+
color: options[:color]
|
|
518
|
+
)
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
def bulk_action(name, **options)
|
|
522
|
+
@bulk_actions << ActionDefinition.new(
|
|
523
|
+
name: name,
|
|
524
|
+
method: options[:method] || :post,
|
|
525
|
+
confirm: options[:confirm],
|
|
526
|
+
type: options[:type] || :button,
|
|
527
|
+
label: options[:label] || name.to_s.humanize,
|
|
528
|
+
icon: options[:icon],
|
|
529
|
+
color: options[:color]
|
|
530
|
+
)
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
ActionDefinition = Struct.new(
|
|
535
|
+
:name, :method, :confirm, :type, :label, :icon, :color,
|
|
536
|
+
:if_condition, :unless_condition,
|
|
537
|
+
keyword_init: true
|
|
538
|
+
)
|
|
539
|
+
end
|
|
540
|
+
end
|
|
541
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AdminSuite
|
|
4
|
+
# Configuration object for AdminSuite.
|
|
5
|
+
class Configuration
|
|
6
|
+
attr_accessor :authenticate,
|
|
7
|
+
:current_actor,
|
|
8
|
+
:authorize,
|
|
9
|
+
:resource_globs,
|
|
10
|
+
:portal_globs,
|
|
11
|
+
:portals,
|
|
12
|
+
:custom_renderers,
|
|
13
|
+
:icon_renderer,
|
|
14
|
+
:docs_url,
|
|
15
|
+
:docs_path,
|
|
16
|
+
:partials,
|
|
17
|
+
:theme,
|
|
18
|
+
:host_stylesheet,
|
|
19
|
+
:tailwind_cdn,
|
|
20
|
+
:on_action_executed,
|
|
21
|
+
:resolve_action_handler
|
|
22
|
+
|
|
23
|
+
def initialize
|
|
24
|
+
@authenticate = nil
|
|
25
|
+
@current_actor = nil
|
|
26
|
+
@authorize = nil
|
|
27
|
+
@resource_globs = []
|
|
28
|
+
@portal_globs = []
|
|
29
|
+
@portals = {}
|
|
30
|
+
@custom_renderers = {}
|
|
31
|
+
@icon_renderer = nil
|
|
32
|
+
@docs_url = nil
|
|
33
|
+
@docs_path = Rails.root.join("docs")
|
|
34
|
+
@partials = {}
|
|
35
|
+
@theme = { primary: :indigo, secondary: :purple }
|
|
36
|
+
@host_stylesheet = nil
|
|
37
|
+
@tailwind_cdn = true
|
|
38
|
+
@on_action_executed = nil
|
|
39
|
+
@resolve_action_handler = nil
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module AdminSuite
|
|
6
|
+
class Engine < ::Rails::Engine
|
|
7
|
+
isolate_namespace AdminSuite
|
|
8
|
+
|
|
9
|
+
initializer "admin_suite.inflections" do
|
|
10
|
+
# Engine namespace uses `UI` (all-caps). Without this, Zeitwerk expects `Ui`.
|
|
11
|
+
ActiveSupport::Inflector.inflections(:en) do |inflect|
|
|
12
|
+
inflect.acronym "UI"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
initializer "admin_suite.host_dsl_ignore" do
|
|
17
|
+
# Host apps sometimes store AdminSuite DSL files under `app/admin_suite/**`.
|
|
18
|
+
# Those are not constant definitions, so we must prevent Zeitwerk from
|
|
19
|
+
# expecting constants like `Portals::Ai` during eager load.
|
|
20
|
+
host_dsl_dir = Rails.root.join("app/admin_suite")
|
|
21
|
+
next unless host_dsl_dir.exist?
|
|
22
|
+
|
|
23
|
+
Rails.autoloaders.each do |loader|
|
|
24
|
+
loader.ignore(host_dsl_dir)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
initializer "admin_suite.admin_dsl" do
|
|
29
|
+
# Ensure core DSL types are loaded in all environments (including test).
|
|
30
|
+
require "admin/base/resource"
|
|
31
|
+
require "admin/base/filter_builder"
|
|
32
|
+
require "admin/base/action_executor"
|
|
33
|
+
require "admin/base/action_handler"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
initializer "admin_suite.watchable_dirs" do |app|
|
|
37
|
+
next unless Rails.env.development?
|
|
38
|
+
|
|
39
|
+
# Make local-engine edits reload without a full server restart.
|
|
40
|
+
app.config.watchable_dirs[root.join("app").to_s] = %w[rb erb js css]
|
|
41
|
+
app.config.watchable_dirs[root.join("lib").to_s] = %w[rb]
|
|
42
|
+
app.config.watchable_dirs[root.join("config").to_s] = %w[rb]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
initializer "admin_suite.assets", before: "propshaft" do |app|
|
|
46
|
+
# Make engine JS/CSS available to the host asset pipeline (Propshaft/Sprockets).
|
|
47
|
+
app.config.assets.paths << root.join("app/javascript")
|
|
48
|
+
app.config.assets.paths << root.join("app/assets")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
initializer "admin_suite.importmap", before: "importmap" do |app|
|
|
52
|
+
# Make engine-provided JS available to host apps using importmap-rails.
|
|
53
|
+
if app.config.respond_to?(:importmap) && app.config.importmap.respond_to?(:paths)
|
|
54
|
+
app.config.importmap.paths << root.join("config/importmap.rb")
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
initializer "admin_suite.configuration" do
|
|
59
|
+
# Provide sensible defaults for host apps.
|
|
60
|
+
AdminSuite.configure do |config|
|
|
61
|
+
if config.resource_globs.blank?
|
|
62
|
+
config.resource_globs = [
|
|
63
|
+
Rails.root.join("config/admin_suite/resources/*.rb").to_s,
|
|
64
|
+
Rails.root.join("app/admin/resources/*.rb").to_s
|
|
65
|
+
]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
if config.portal_globs.blank?
|
|
69
|
+
config.portal_globs = [
|
|
70
|
+
Rails.root.join("config/admin_suite/portals/*.rb").to_s,
|
|
71
|
+
Rails.root.join("app/admin/portals/*.rb").to_s,
|
|
72
|
+
Rails.root.join("app/admin_suite/portals/*.rb").to_s
|
|
73
|
+
]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
config.portals = {
|
|
77
|
+
ops: { label: "Ops Portal", icon: "settings", color: :amber, order: 10 },
|
|
78
|
+
email: { label: "Email Portal", icon: "inbox", color: :emerald, order: 20 },
|
|
79
|
+
ai: { label: "AI Portal", icon: "cpu", color: :cyan, order: 30 },
|
|
80
|
+
assistant: { label: "Assistant Portal", icon: "message-circle", color: :violet, order: 40 }
|
|
81
|
+
} if config.portals.blank?
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
initializer "admin_suite.tailwind_build" do
|
|
86
|
+
next unless Rails.env.development?
|
|
87
|
+
|
|
88
|
+
# In development, ensure the engine stylesheet exists so the UI is usable
|
|
89
|
+
# without requiring host-specific Tailwind setup.
|
|
90
|
+
output = Rails.root.join("app/assets/builds/admin_suite_tailwind.css")
|
|
91
|
+
next if output.exist?
|
|
92
|
+
|
|
93
|
+
input = root.join("app/assets/tailwind/admin_suite.css")
|
|
94
|
+
FileUtils.mkdir_p(output.dirname)
|
|
95
|
+
|
|
96
|
+
system("tailwindcss", "-i", input.to_s, "-o", output.to_s)
|
|
97
|
+
rescue StandardError
|
|
98
|
+
# Best effort only; missing stylesheet will show up immediately in the UI.
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|