zen_admin 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +76 -0
  3. data/Rakefile +6 -0
  4. data/app/assets/stylesheets/zen_admin/application.css +15 -0
  5. data/app/controllers/zen_admin/application_controller.rb +27 -0
  6. data/app/controllers/zen_admin/dashboard_controller.rb +9 -0
  7. data/app/controllers/zen_admin/sessions_controller.rb +40 -0
  8. data/app/controllers/zen_admin/ui/resources_controller.rb +331 -0
  9. data/app/helpers/zen_admin/application_helper.rb +80 -0
  10. data/app/javascript/controllers/application.js +9 -0
  11. data/app/javascript/controllers/bulk_actions_controller.js +55 -0
  12. data/app/javascript/controllers/clipboard_controller.js +27 -0
  13. data/app/javascript/controllers/index.js +5 -0
  14. data/app/javascript/controllers/layout_controller.js +48 -0
  15. data/app/javascript/controllers/menu_group_controller.js +40 -0
  16. data/app/javascript/controllers/modal_controller.js +31 -0
  17. data/app/javascript/controllers/toast_controller.js +14 -0
  18. data/app/javascript/controllers/tom_select_controller.js +35 -0
  19. data/app/javascript/zen_admin/application.js +12 -0
  20. data/app/jobs/zen_admin/application_job.rb +4 -0
  21. data/app/mailers/zen_admin/application_mailer.rb +6 -0
  22. data/app/models/zen_admin/application_record.rb +5 -0
  23. data/app/models/zen_admin/asset.rb +43 -0
  24. data/app/models/zen_admin/audit_log.rb +116 -0
  25. data/app/models/zen_admin/permission.rb +73 -0
  26. data/app/models/zen_admin/role.rb +45 -0
  27. data/app/models/zen_admin/trash_item.rb +98 -0
  28. data/app/models/zen_admin/user.rb +37 -0
  29. data/app/policies/zen_admin/application_policy.rb +53 -0
  30. data/app/policies/zen_admin/resource_policy.rb +48 -0
  31. data/app/views/layouts/zen_admin/_flash.html.erb +9 -0
  32. data/app/views/layouts/zen_admin/_sidebar.html.erb +115 -0
  33. data/app/views/layouts/zen_admin/application.html.erb +98 -0
  34. data/app/views/zen_admin/builtin/_copy_button.html.erb +9 -0
  35. data/app/views/zen_admin/builtin/_file_link.html.erb +22 -0
  36. data/app/views/zen_admin/dashboard/index.html.erb +27 -0
  37. data/app/views/zen_admin/sessions/new.html.erb +47 -0
  38. data/app/views/zen_admin/ui/resources/_form.html.erb +226 -0
  39. data/app/views/zen_admin/ui/resources/_row.html.erb +170 -0
  40. data/app/views/zen_admin/ui/resources/create.turbo_stream.erb +2 -0
  41. data/app/views/zen_admin/ui/resources/destroy.turbo_stream.erb +2 -0
  42. data/app/views/zen_admin/ui/resources/edit.html.erb +11 -0
  43. data/app/views/zen_admin/ui/resources/index.html.erb +285 -0
  44. data/app/views/zen_admin/ui/resources/new.html.erb +11 -0
  45. data/app/views/zen_admin/ui/resources/show.html.erb +133 -0
  46. data/app/views/zen_admin/ui/resources/update.turbo_stream.erb +2 -0
  47. data/config/importmap.rb +10 -0
  48. data/config/locales/en.yml +107 -0
  49. data/config/locales/zh-CN.yml +110 -0
  50. data/config/routes.rb +24 -0
  51. data/lib/generators/zen_admin/admin_user/admin_user_generator.rb +55 -0
  52. data/lib/generators/zen_admin/install/install_generator.rb +50 -0
  53. data/lib/generators/zen_admin/install/templates/asset.rb +46 -0
  54. data/lib/generators/zen_admin/install/templates/create_zen_admin_assets.rb.erb +9 -0
  55. data/lib/generators/zen_admin/install/templates/create_zen_admin_audit_logs.rb.erb +18 -0
  56. data/lib/generators/zen_admin/install/templates/create_zen_admin_rbac.rb.erb +57 -0
  57. data/lib/generators/zen_admin/install/templates/create_zen_admin_trash_items.rb.erb +13 -0
  58. data/lib/generators/zen_admin/install/templates/zen_admin.rb +17 -0
  59. data/lib/generators/zen_admin/install/templates/zh-CN.yml +15 -0
  60. data/lib/generators/zen_admin/model/model_generator.rb +65 -0
  61. data/lib/generators/zen_admin/model/templates/zen_admin_config.rb.erb +80 -0
  62. data/lib/generators/zen_admin/rbac_install/rbac_install_generator.rb +52 -0
  63. data/lib/generators/zen_admin/rbac_install/templates/create_zen_admin_rbac.rb.erb +42 -0
  64. data/lib/generators/zen_admin/rbac_install/templates/seeds.rb.erb +13 -0
  65. data/lib/tasks/zen_admin_tasks.rake +4 -0
  66. data/lib/zen_admin/authenticatable.rb +44 -0
  67. data/lib/zen_admin/builtin.rb +77 -0
  68. data/lib/zen_admin/configuration.rb +17 -0
  69. data/lib/zen_admin/core/field.rb +34 -0
  70. data/lib/zen_admin/core/filter.rb +16 -0
  71. data/lib/zen_admin/core/resource.rb +175 -0
  72. data/lib/zen_admin/core.rb +3 -0
  73. data/lib/zen_admin/engine.rb +67 -0
  74. data/lib/zen_admin/link_registerable.rb +11 -0
  75. data/lib/zen_admin/registerable.rb +17 -0
  76. data/lib/zen_admin/registry.rb +83 -0
  77. data/lib/zen_admin/schema/serializer.rb +15 -0
  78. data/lib/zen_admin/schema.rb +1 -0
  79. data/lib/zen_admin/version.rb +3 -0
  80. data/lib/zen_admin.rb +51 -0
  81. metadata +233 -0
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :zen_admin do
3
+ # # Task goes here
4
+ # end
@@ -0,0 +1,44 @@
1
+ module ZenAdmin
2
+ module Authenticatable
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ helper_method :current_admin_user if respond_to?(:helper_method)
7
+ end
8
+
9
+ private
10
+
11
+ def current_admin_user
12
+ @current_admin_user ||= if session[:zen_admin_user_id] == "config_admin"
13
+ # 定义一个简单的类来模拟拥有 can? 方法的用户
14
+ Class.new do
15
+ def username; ZenAdmin.configuration.username; end
16
+ def can?(_permission); true; end # 硬编码管理员拥有所有权限
17
+ def super_admin?; true; end
18
+ def id; "config_admin"; end
19
+ end.new
20
+ elsif session[:zen_admin_user_id]
21
+ ZenAdmin::User.find_by(id: session[:zen_admin_user_id])
22
+ end
23
+ end
24
+
25
+ def authenticate_admin!
26
+ unless current_admin_user
27
+ redirect_to zen_admin.login_path
28
+ end
29
+ end
30
+
31
+ # 手动记录审计日志的快捷方法
32
+ def zen_admin_audit(resource, action, note: nil, changes: nil)
33
+ @zen_admin_audited = true # 标记已手动审计
34
+ ZenAdmin::AuditLog.record(
35
+ current_admin_user,
36
+ resource,
37
+ action,
38
+ note: note,
39
+ changes: changes,
40
+ request: request
41
+ )
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,77 @@
1
+ module ZenAdmin
2
+ module Builtin
3
+ def self.register_active_storage_blob
4
+ return unless defined?(ActiveStorage::Attachment)
5
+ return unless ActiveRecord::Base.connection.table_exists?("active_storage_attachments")
6
+
7
+ # 锁定保护
8
+ ActiveStorage::Attachment.send(:define_method, :zen_admin_editable?) { false }
9
+ ActiveStorage::Blob.send(:define_method, :zen_admin_editable?) { false }
10
+
11
+ # 1. 注册 Blob (完全隐藏,仅作为底层支撑)
12
+ ZenAdmin.register(ActiveStorage::Blob) do |resource|
13
+ resource.menu visible: false
14
+ resource.show do
15
+ field :id, label: "文件链接" do |blob|
16
+ url = Rails.application.routes.url_helpers.rails_blob_url(blob, host: "localhost:3000") rescue ""
17
+ render "zen_admin/builtin/file_link", url: url
18
+ end
19
+ end
20
+ end
21
+
22
+ # 2. Register System Attachments
23
+ ZenAdmin.register(ActiveStorage::Attachment) do |resource|
24
+ resource.label :'zen_admin.builtin.attachments', :'zen_admin.builtin.attachments'
25
+ resource.menu label: :'zen_admin.builtin.attachments', icon: "fas fa-robot", position: 101, group: "系统管理"
26
+ resource.soft_delete = false
27
+ resource.includes :blob, :record
28
+ resource.enable_batch_actions
29
+ resource.batch_action :delete, label: :'zen_admin.actions.delete', type: :danger, confirm: I18n.t('zen_admin.ui.confirm_bulk_delete') do |records|
30
+ records.destroy_all
31
+ end
32
+
33
+ resource.list do
34
+ field :id, label: :'zen_admin.models.active_storage/attachment.fields.id' do |att|
35
+ if att.blob.representable?
36
+ url = main_app.rails_blob_path(att.blob, only_path: true)
37
+ full_url = main_app.url_for(att.blob)
38
+ tag.div(class: "thumbnail-box border rounded cursor-pointer shadow-xs",
39
+ style: "width: 35px; height: 35px; overflow: hidden;",
40
+ data: { action: "click->layout#previewImage", preview_url: full_url }) do
41
+ image_tag(url, style: "width: 100%; height: 100%; object-fit: cover;")
42
+ end
43
+ else
44
+ # 精细化文件图标显示
45
+ tag.i(class: "fas #{case att.blob.content_type
46
+ when /pdf/ then 'fa-file-pdf text-danger'
47
+ when /word|officedocument/ then 'fa-file-word text-primary'
48
+ when /excel|sheet/ then 'fa-file-excel text-success'
49
+ else 'fa-file-alt'
50
+ end} fa-lg")
51
+ end
52
+ end
53
+ field :record_type, label: :'zen_admin.models.active_storage/attachment.fields.record_type'
54
+ field :name, label: :'zen_admin.models.active_storage/attachment.fields.name'
55
+ field :blob_filename, label: :'zen_admin.models.active_storage/attachment.fields.blob_filename' do |att|
56
+ att.blob.filename.to_s
57
+ end
58
+ field :blob_id, label: :'zen_admin.models.active_storage/attachment.fields.blob_id' do |att|
59
+ url = Rails.application.routes.url_helpers.rails_blob_url(att.blob, host: "localhost:3000") rescue "#"
60
+ render "zen_admin/builtin/copy_button", url: url
61
+ end
62
+ end
63
+
64
+ resource.show do
65
+ field :record, label: :'zen_admin.models.active_storage/attachment.fields.record'
66
+ field :blob, label: :'zen_admin.models.active_storage/attachment.fields.blob' do |att|
67
+ url = Rails.application.routes.url_helpers.rails_blob_url(att.blob, host: "localhost:3000") rescue ""
68
+ render "zen_admin/builtin/file_link", url: url
69
+ end
70
+ end
71
+ end
72
+
73
+ # 3. 显式加载我们的手动资源库模型
74
+ require_dependency ZenAdmin::Engine.root.join("app/models/zen_admin/asset")
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,17 @@
1
+ module ZenAdmin
2
+ class Configuration
3
+ attr_accessor :enable_ui, :admin_path, :username, :password, :rbac_enable, :default_locale, :custom_dashboard, :audit_log_enable, :time_zone
4
+
5
+ def initialize
6
+ @enable_ui = true
7
+ @admin_path = "/admin"
8
+ @username = "admin"
9
+ @password = "password"
10
+ @rbac_enable = false
11
+ @default_locale = :"zh-CN"
12
+ @custom_dashboard = nil
13
+ @audit_log_enable = true
14
+ @time_zone = "Beijing"
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,34 @@
1
+ module ZenAdmin
2
+ module Core
3
+ class Field
4
+ attr_accessor :name, :type, :options, :required, :condition
5
+
6
+ def initialize(name, type: :string, options: {}, required: false, condition: nil, label: nil)
7
+ @name = name
8
+ @type = type
9
+ @options = options
10
+ @options[:label] = label if label
11
+ @required = required
12
+ @condition = condition
13
+ end
14
+
15
+ def label
16
+ val = @options[:label]
17
+ if val.is_a?(Symbol)
18
+ I18n.t(val)
19
+ else
20
+ val || @name.to_s.humanize
21
+ end
22
+ end
23
+
24
+ def to_h
25
+ {
26
+ name: @name.to_s,
27
+ type: @type.to_s,
28
+ required: @required,
29
+ options: []
30
+ }
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,16 @@
1
+ module ZenAdmin
2
+ module Core
3
+ class Filter
4
+ attr_accessor :name, :type
5
+
6
+ def initialize(name, type: :string)
7
+ @name = name
8
+ @type = type
9
+ end
10
+
11
+ def to_h
12
+ { name: @name.to_s, type: @type.to_s }
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,175 @@
1
+ module ZenAdmin
2
+ module Core
3
+ class Resource
4
+ attr_accessor :name, :list_fields, :form_fields, :show_fields, :search_fields, :filters, :actions, :menu_options, :includes_list, :batch_actions, :member_actions, :resource_label_singular, :resource_label_plural, :soft_delete, :model_name, :scopes_list, :scopes_proc, :exportable
5
+
6
+ def initialize(model)
7
+ @model_name = model.to_s
8
+ if model.respond_to?(:name)
9
+ @name = model.name.underscore.pluralize
10
+ else
11
+ @name = model.to_s.underscore.pluralize
12
+ end
13
+
14
+ begin
15
+ @resource_label_singular = model.model_name.human
16
+ @resource_label_plural = model.model_name.human.pluralize
17
+ rescue
18
+ @resource_label_singular = model.to_s.humanize
19
+ @resource_label_plural = @resource_label_singular.pluralize
20
+ end
21
+
22
+ @list_fields = []
23
+ @form_fields = []
24
+ @show_fields = []
25
+ @search_fields = []
26
+ @filters = []
27
+ @actions = [:index, :new, :create, :edit, :update, :show, :destroy]
28
+ @menu_options = { visible: true, label: nil, icon: nil, position: 10 }
29
+ @includes_list = []
30
+ @batch_actions = {}
31
+ @member_actions = {}
32
+ @scopes_list = []
33
+ @scopes_proc = nil
34
+ @soft_delete = true
35
+ @exportable = false
36
+ end
37
+
38
+ def label(singular = nil, plural = nil)
39
+ if singular
40
+ @resource_label_singular = singular
41
+ @resource_label_plural = plural || singular
42
+ else
43
+ translate_if_symbol(@resource_label_singular)
44
+ end
45
+ end
46
+
47
+ def label_plural
48
+ translate_if_symbol(@resource_label_plural)
49
+ end
50
+
51
+ def translate_if_symbol(val)
52
+ if val.is_a?(Symbol)
53
+ I18n.t(val, default: [model.model_name.human, val.to_s.split('.').last.humanize])
54
+ else
55
+ val
56
+ end
57
+ end
58
+
59
+ def exportable!
60
+ @exportable = true
61
+ end
62
+
63
+ def scope(name_or_proc = nil, label: nil, query: {}, default: false, &block)
64
+ if block_given?
65
+ @scopes_proc = block
66
+ elsif name_or_proc.is_a?(Proc)
67
+ @scopes_proc = name_or_proc
68
+ else
69
+ @scopes_list << { name: name_or_proc.to_sym, label: label, query: query, default: default }
70
+ end
71
+ end
72
+
73
+ def computed_scopes(context = nil)
74
+ results = @scopes_list.dup
75
+ if @scopes_proc
76
+ dynamic_results = context ? context.instance_exec(&@scopes_proc) : @scopes_proc.call
77
+ results += Array(dynamic_results)
78
+ end
79
+ results
80
+ end
81
+
82
+ def batch_actions; @batch_actions || {}; end
83
+ def member_actions; @member_actions || {}; end
84
+
85
+ def member_action(name_or_array, label: nil, type: :default, icon: nil, confirm: nil, method: nil, url: nil, handler: nil, &block)
86
+ if name_or_array.is_a?(Array)
87
+ name_or_array.each do |config|
88
+ name = config[:name]
89
+ @member_actions[name.to_sym] = {
90
+ name: name.to_sym,
91
+ label: config[:label],
92
+ icon: config[:icon],
93
+ type: config[:type] || :default,
94
+ confirm: config[:confirm],
95
+ method: config[:method],
96
+ url: config[:url],
97
+ handler: config[:handler]
98
+ }
99
+ end
100
+ else
101
+ @member_actions[name_or_array.to_sym] = {
102
+ name: name_or_array.to_sym,
103
+ label: label,
104
+ icon: icon,
105
+ type: type,
106
+ confirm: confirm,
107
+ method: method,
108
+ url: url,
109
+ handler: handler || block
110
+ }
111
+ end
112
+ end
113
+
114
+ def enable_batch_actions(enabled = true)
115
+ @batch_actions = {} if enabled && (@batch_actions.nil? || @batch_actions == false)
116
+ end
117
+
118
+ def batch_action(name_or_array, label: nil, type: :default, confirm: nil, method: nil, url: nil, handler: nil, &block)
119
+ if name_or_array.is_a?(Array)
120
+ name_or_array.each do |config|
121
+ name = config[:name]
122
+ @batch_actions[name.to_sym] = {
123
+ name: name.to_sym,
124
+ label: config[:label],
125
+ type: config[:type] || :default,
126
+ confirm: config[:confirm],
127
+ method: config[:method],
128
+ url: config[:url],
129
+ handler: config[:handler]
130
+ }
131
+ end
132
+ else
133
+ @batch_actions[name_or_array.to_sym] = {
134
+ name: name_or_array.to_sym,
135
+ label: label,
136
+ type: type,
137
+ confirm: confirm,
138
+ method: method,
139
+ url: url,
140
+ handler: handler || block
141
+ }
142
+ end
143
+ end
144
+
145
+ def search(&block); end
146
+ def includes(*args); @includes_list.concat(args); end
147
+
148
+ def menu(visible: true, label: nil, icon: nil, position: 10, group: nil)
149
+ @menu_options = { visible: visible, label: label, icon: icon, position: position, group: group }
150
+ end
151
+
152
+ def model; @model_name.constantize; end
153
+ def show(&block); dsl = FieldDSL.new(self, :show_fields); dsl.instance_eval(&block); end
154
+ def list(&block); dsl = FieldDSL.new(self, :list_fields); dsl.instance_eval(&block); end
155
+ def form(&block); dsl = FieldDSL.new(self, :form_fields); dsl.instance_eval(&block); end
156
+ def filters(&block)
157
+ if block_given?
158
+ dsl = FieldDSL.new(self, :filters); dsl.instance_eval(&block)
159
+ else
160
+ @filters || []
161
+ end
162
+ end
163
+ def filter_fields; @filters || []; end
164
+
165
+ class FieldDSL
166
+ def initialize(resource, target_attr); @resource = resource; @target = target_attr; end
167
+ def field(name, type: :string, required: false, condition: nil, **options, &block)
168
+ field_obj = ZenAdmin::Core::Field.new(name, type: type, options: options, required: required, condition: condition)
169
+ field_obj.options[:formatter] = block if block_given?
170
+ @resource.send(@target) << field_obj
171
+ end
172
+ end
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,3 @@
1
+ require "zen_admin/core/field"
2
+ require "zen_admin/core/filter"
3
+ require "zen_admin/core/resource"
@@ -0,0 +1,67 @@
1
+ module ZenAdmin
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace ZenAdmin
4
+
5
+ initializer "zen_admin.i18n" do |app|
6
+ app.config.i18n.default_locale = ZenAdmin.configuration.default_locale
7
+ # Add engine locales to host app
8
+ app.config.i18n.load_path += Dir[root.join("config", "locales", "**", "*.{rb,yml}")]
9
+
10
+ # 设置 Rails 时区
11
+ Time.zone = ZenAdmin.configuration.time_zone
12
+ Groupdate.time_zone = Time.zone if defined?(Groupdate)
13
+ end
14
+
15
+ initializer "zen_admin.active_storage_csrf_fix" do
16
+ ActiveSupport.on_load(:active_storage_direct_uploads_controller) do
17
+ skip_before_action :verify_authenticity_token, raise: false
18
+ end
19
+ end
20
+
21
+ initializer "zen_admin.assets" do |app|
22
+ app.config.assets.paths << root.join("app/javascript")
23
+ end
24
+
25
+ initializer "zen_admin.importmap", before: "importmap" do |app|
26
+ if app.config.respond_to?(:importmap)
27
+ app.config.importmap.paths << root.join("config/importmap.rb")
28
+ app.config.importmap.cache_sweepers << root.join("app/javascript")
29
+ end
30
+ end
31
+
32
+ initializer "zen_admin.auto_discover_models" do
33
+ Rails.application.config.to_prepare do
34
+ # 1. 加载内置资源逻辑
35
+ require_dependency ZenAdmin::Engine.root.join("lib/zen_admin/builtin")
36
+ ZenAdmin::Builtin.register_active_storage_blob rescue nil
37
+
38
+ # 2. 显式加载所有内置模型 (确保注册到 Registry)
39
+ require_dependency ZenAdmin::Engine.root.join("app/models/zen_admin/asset")
40
+ require_dependency ZenAdmin::Engine.root.join("app/models/zen_admin/trash_item")
41
+ require_dependency ZenAdmin::Engine.root.join("app/models/zen_admin/audit_log")
42
+
43
+ if ZenAdmin.configuration.rbac_enable
44
+ require_dependency ZenAdmin::Engine.root.join("app/models/zen_admin/user")
45
+ require_dependency ZenAdmin::Engine.root.join("app/models/zen_admin/role")
46
+ require_dependency ZenAdmin::Engine.root.join("app/models/zen_admin/permission")
47
+ end
48
+
49
+ # 3. 扫描并加载宿主应用的模型
50
+ files = Dir.glob(Rails.root.join("app/models/**/*.rb")) + Dir.glob(Rails.root.join("app/controllers/**/*.rb"))
51
+ files.each do |file|
52
+ next if File.directory?(file)
53
+ begin
54
+ require_dependency file
55
+ rescue => e
56
+ Rails.logger.debug "[ZenAdmin] Failed to load #{file}: #{e.message}"
57
+ end
58
+ end
59
+
60
+ # 4. 自动同步权限点到数据库
61
+ if ZenAdmin.configuration.rbac_enable
62
+ ZenAdmin::Permission.sync_from_registry! rescue nil
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,11 @@
1
+ module ZenAdmin
2
+ module LinkRegisterable
3
+ extend ActiveSupport::Concern
4
+
5
+ class_methods do
6
+ def zen_admin_link(label, path, icon: "fas fa-link", visible: true, position: 10, permission: nil)
7
+ ZenAdmin.register_link(label, path, icon: icon, visible: visible, position: position, permission: permission)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,17 @@
1
+ module ZenAdmin
2
+ module Registerable
3
+ extend ActiveSupport::Concern
4
+
5
+ class_methods do
6
+ def zen_admin(&block)
7
+ ZenAdmin.register(self, &block)
8
+
9
+ # 自动授权 Ransack 搜索权限
10
+ if respond_to?(:ransackable_attributes)
11
+ define_singleton_method(:ransackable_attributes) { |auth_object = nil| column_names }
12
+ define_singleton_method(:ransackable_associations) { |auth_object = nil| reflect_on_all_associations.map { |a| a.name.to_s } }
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,83 @@
1
+ require "singleton"
2
+
3
+ module ZenAdmin
4
+ class Registry
5
+ include Singleton
6
+
7
+ def initialize
8
+ @resources = {}
9
+ @links = []
10
+ end
11
+
12
+ def register(resource)
13
+ @resources[resource.model_name.to_s] = resource
14
+ end
15
+
16
+ def register_link(label, path, icon:, visible:, position: 10, permission: nil)
17
+ @links.reject! { |l| l[:label] == label || l[:path] == path }
18
+ @links << { label: label, path: path, icon: icon, visible: visible, position: position, permission: permission }
19
+ end
20
+
21
+ def menu_items
22
+ raw_items = []
23
+
24
+ # Collect resources
25
+ all.each do |r|
26
+ next unless r.respond_to?(:menu_options) && r.menu_options[:visible]
27
+ raw_items << {
28
+ type: :resource,
29
+ object: r,
30
+ position: r.menu_options[:position] || 10,
31
+ group: r.menu_options[:group]
32
+ }
33
+ end
34
+
35
+ # Collect links
36
+ @links.each do |l|
37
+ next unless l[:visible]
38
+ raw_items << {
39
+ type: :link,
40
+ object: l,
41
+ position: l[:position] || 10,
42
+ group: nil # Links currently don't support grouping in this iteration, or we can add it later
43
+ }
44
+ end
45
+
46
+ # Grouping logic
47
+ grouped_items = {}
48
+ ungrouped_items = []
49
+
50
+ raw_items.each do |item|
51
+ if item[:group].present?
52
+ group_name = item[:group]
53
+ grouped_items[group_name] ||= { type: :group, label: group_name, items: [], position: nil }
54
+ grouped_items[group_name][:items] << item
55
+ else
56
+ ungrouped_items << item
57
+ end
58
+ end
59
+
60
+ # Calculate position for groups (min position of children)
61
+ grouped_items.each do |_, group|
62
+ group[:items].sort_by! { |i| i[:position] }
63
+ group[:position] = group[:items].first[:position]
64
+ end
65
+
66
+ # Merge and sort
67
+ final_list = ungrouped_items + grouped_items.values
68
+ final_list.sort_by { |i| i[:position] }
69
+ end
70
+
71
+ def [](model_or_name)
72
+ @resources[model_or_name.to_s]
73
+ end
74
+
75
+ def all
76
+ @resources.values
77
+ end
78
+
79
+ def links
80
+ @links
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,15 @@
1
+ module ZenAdmin
2
+ module Schema
3
+ class Serializer
4
+ def self.serialize(resource)
5
+ {
6
+ model: resource.model.name,
7
+ list: resource.list_fields.map(&:to_h),
8
+ form: resource.form_fields.map(&:to_h),
9
+ filters: resource.filters.map(&:to_h),
10
+ actions: resource.actions.map(&:to_h)
11
+ }
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1 @@
1
+ require "zen_admin/schema/serializer"
@@ -0,0 +1,3 @@
1
+ module ZenAdmin
2
+ VERSION = "0.9.2"
3
+ end
data/lib/zen_admin.rb ADDED
@@ -0,0 +1,51 @@
1
+ require "zen_admin/version"
2
+ require "zen_admin/engine"
3
+ require "zen_admin/configuration"
4
+ require "zen_admin/core"
5
+ require "zen_admin/registry"
6
+ require "zen_admin/schema"
7
+ require "zen_admin/registerable"
8
+ require "zen_admin/link_registerable"
9
+ require "zen_admin/authenticatable"
10
+ require "kaminari"
11
+ require "pundit"
12
+ require "ransack"
13
+
14
+ module ZenAdmin
15
+ class << self
16
+ def configuration
17
+ @configuration ||= Configuration.new
18
+ end
19
+
20
+ def configure
21
+ yield configuration
22
+ end
23
+
24
+ def register(model, &block)
25
+ resource = Core::Resource.new(model)
26
+ block.call(resource) if block_given?
27
+ Registry.instance.register(resource)
28
+ end
29
+
30
+ def register_link(label, path, icon: "fas fa-link", visible: true, position: 10, permission: nil)
31
+ Registry.instance.register_link(label, path, icon: icon, visible: visible, position: position, permission: permission)
32
+ end
33
+ end
34
+ end
35
+
36
+ # Auto-include in ActiveRecord::Base
37
+ ActiveSupport.on_load(:active_record) do
38
+ include ZenAdmin::Registerable
39
+ end
40
+
41
+ # Auto-include in ActionController::Base
42
+
43
+ ActiveSupport.on_load(:action_controller) do
44
+
45
+ include ZenAdmin::LinkRegisterable
46
+
47
+ include ZenAdmin::Authenticatable
48
+
49
+ helper ZenAdmin::ApplicationHelper
50
+
51
+ end