administratum 0.2.4

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 (88) hide show
  1. checksums.yaml +7 -0
  2. data/DESIGN.md +68 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +431 -0
  5. data/Rakefile +6 -0
  6. data/app/admin/administratum/user_admin.rb +39 -0
  7. data/app/assets/builds/administratum.css +2 -0
  8. data/app/assets/javascripts/administratum.js +250 -0
  9. data/app/assets/tailwind/administratum/components.css +199 -0
  10. data/app/assets/tailwind/administratum/index.css +8 -0
  11. data/app/assets/tailwind/administratum/theme.css +62 -0
  12. data/app/controllers/administratum/application_controller.rb +31 -0
  13. data/app/controllers/administratum/dashboard_controller.rb +7 -0
  14. data/app/controllers/administratum/page.rb +51 -0
  15. data/app/controllers/administratum/password_resets_controller.rb +42 -0
  16. data/app/controllers/administratum/resource.rb +224 -0
  17. data/app/controllers/administratum/sessions_controller.rb +33 -0
  18. data/app/helpers/administratum/application_helper.rb +11 -0
  19. data/app/helpers/administratum/form_helper.rb +115 -0
  20. data/app/helpers/administratum/island_helper.rb +16 -0
  21. data/app/helpers/administratum/layout_helper.rb +81 -0
  22. data/app/helpers/administratum/resource_helper.rb +199 -0
  23. data/app/mailers/administratum/application_mailer.rb +8 -0
  24. data/app/mailers/administratum/user_mailer.rb +11 -0
  25. data/app/models/administratum/application_record.rb +7 -0
  26. data/app/models/administratum/user.rb +13 -0
  27. data/app/views/administratum/dashboard/index.html.erb +16 -0
  28. data/app/views/administratum/fields/_belongs_to.html.erb +1 -0
  29. data/app/views/administratum/fields/_boolean.html.erb +1 -0
  30. data/app/views/administratum/fields/_date.html.erb +1 -0
  31. data/app/views/administratum/fields/_datetime.html.erb +1 -0
  32. data/app/views/administratum/fields/_email.html.erb +1 -0
  33. data/app/views/administratum/fields/_has_many_ids.html.erb +1 -0
  34. data/app/views/administratum/fields/_hidden.html.erb +1 -0
  35. data/app/views/administratum/fields/_island.html.erb +4 -0
  36. data/app/views/administratum/fields/_json.html.erb +1 -0
  37. data/app/views/administratum/fields/_multi_select.html.erb +1 -0
  38. data/app/views/administratum/fields/_number.html.erb +1 -0
  39. data/app/views/administratum/fields/_password.html.erb +1 -0
  40. data/app/views/administratum/fields/_select.html.erb +1 -0
  41. data/app/views/administratum/fields/_string.html.erb +1 -0
  42. data/app/views/administratum/fields/_textarea.html.erb +1 -0
  43. data/app/views/administratum/fields/_wrapper.html.erb +11 -0
  44. data/app/views/administratum/pages/index.html.erb +7 -0
  45. data/app/views/administratum/password_resets/edit.html.erb +17 -0
  46. data/app/views/administratum/password_resets/new.html.erb +11 -0
  47. data/app/views/administratum/resources/_attributes.html.erb +11 -0
  48. data/app/views/administratum/resources/_form.html.erb +14 -0
  49. data/app/views/administratum/resources/_table.html.erb +42 -0
  50. data/app/views/administratum/resources/edit.html.erb +7 -0
  51. data/app/views/administratum/resources/index.html.erb +17 -0
  52. data/app/views/administratum/resources/new.html.erb +7 -0
  53. data/app/views/administratum/resources/show.html.erb +7 -0
  54. data/app/views/administratum/sessions/new.html.erb +18 -0
  55. data/app/views/administratum/shared/_card.html.erb +9 -0
  56. data/app/views/administratum/shared/_filters.html.erb +43 -0
  57. data/app/views/administratum/shared/_flash.html.erb +9 -0
  58. data/app/views/administratum/shared/_form_errors.html.erb +10 -0
  59. data/app/views/administratum/shared/_nav.html.erb +10 -0
  60. data/app/views/administratum/shared/_page_header.html.erb +17 -0
  61. data/app/views/administratum/shared/_panel.html.erb +24 -0
  62. data/app/views/administratum/shared/_topbar.html.erb +29 -0
  63. data/app/views/administratum/user_mailer/reset_password.html.erb +4 -0
  64. data/app/views/administratum/user_mailer/reset_password.text.erb +7 -0
  65. data/app/views/layouts/administratum/application.html.erb +25 -0
  66. data/app/views/layouts/administratum/authentication.html.erb +32 -0
  67. data/config/locales/en.yml +65 -0
  68. data/config/routes.rb +16 -0
  69. data/db/migrate/20261016000000_create_administratum_users.rb +17 -0
  70. data/lib/administratum/configuration.rb +66 -0
  71. data/lib/administratum/definition.rb +283 -0
  72. data/lib/administratum/dsl.rb +195 -0
  73. data/lib/administratum/engine.rb +31 -0
  74. data/lib/administratum/field_types.rb +77 -0
  75. data/lib/administratum/page_dsl.rb +48 -0
  76. data/lib/administratum/paths.rb +85 -0
  77. data/lib/administratum/ransackable.rb +41 -0
  78. data/lib/administratum/registry.rb +95 -0
  79. data/lib/administratum/routing.rb +57 -0
  80. data/lib/administratum/tailwind.rb +34 -0
  81. data/lib/administratum/version.rb +5 -0
  82. data/lib/administratum.rb +43 -0
  83. data/lib/generators/administratum/install/install_generator.rb +42 -0
  84. data/lib/generators/administratum/install/templates/administratum.rb +41 -0
  85. data/lib/generators/administratum/resource/resource_generator.rb +30 -0
  86. data/lib/generators/administratum/resource/templates/resource.rb.tt +20 -0
  87. data/lib/tasks/administratum.rake +32 -0
  88. metadata +209 -0
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Administratum
4
+ # Class-level DSL for custom pages (Administratum::Page subclasses in app/admin).
5
+ module PageDSL
6
+ include DSL::Common
7
+
8
+ def definition
9
+ @definition ||= superclass.respond_to?(:definition) ? superclass.definition.dup : Definition.new
10
+ end
11
+
12
+ # Marks a class as a base for other pages: it gets no route or menu entry.
13
+ def abstract_page!
14
+ @abstract_page = true
15
+ end
16
+
17
+ def abstract_page?
18
+ @abstract_page == true || name == "Administratum::Page"
19
+ end
20
+
21
+ def label(text)
22
+ definition.label = text
23
+ end
24
+
25
+ # An extra route on the page, /<route_key>/<name>, handled by the method of that name.
26
+ def page_action(name, method: :get)
27
+ definition.add_action(name, http_method: method, scope: :collection)
28
+ end
29
+
30
+ # Buttons on a page default to its header.
31
+ def button(name, on: :page, **)
32
+ super
33
+ end
34
+
35
+ # ReportsPage => "reports"; Stats::WeeklyPage => "stats_weekly".
36
+ def route_key
37
+ definition.route_key || name.delete_suffix("Page").underscore.tr("/", "_")
38
+ end
39
+
40
+ def page_title
41
+ definition.label || route_key.humanize
42
+ end
43
+
44
+ def menu_label
45
+ definition.menu.label || page_title
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Administratum
4
+ # Builds paths for a resource class from the route names Routing draws.
5
+ # `context` is anything that responds to the engine's *_path helpers.
6
+ class Paths
7
+ def initialize(resource, context)
8
+ @resource = resource
9
+ @context = context
10
+ end
11
+
12
+ def index(parent = nil, **query)
13
+ call(collection_name, nested? ? [parent] : [], query)
14
+ end
15
+
16
+ def new(parent = nil, **query)
17
+ call("new_#{nested? ? "#{parent_key}_" : ''}#{singular}", nested? ? [parent] : [], query)
18
+ end
19
+
20
+ def show(record, **query)
21
+ call(member_name, member_args(record), query)
22
+ end
23
+
24
+ def edit(record, **query)
25
+ call("edit_#{member_name}", member_args(record), query)
26
+ end
27
+
28
+ # Path for a custom action. Member actions take the record, collection
29
+ # actions take the parent (when nested) or nothing.
30
+ def action(name, record_or_parent = nil, **query)
31
+ action = @resource.definition.custom_actions.fetch(name.to_sym)
32
+ if action.member?
33
+ call("#{name}_#{member_name}", member_args(record_or_parent), query)
34
+ else
35
+ call("#{name}_#{collection_name}", nested? ? [record_or_parent] : [], query)
36
+ end
37
+ end
38
+
39
+ # Where a form for `record` submits to.
40
+ def form(record, parent = nil)
41
+ record.persisted? ? show(record) : index(parent || parent_of(record))
42
+ end
43
+
44
+ def parent_of(record)
45
+ return unless nested?
46
+
47
+ record.public_send(@resource.definition.parent.name)
48
+ end
49
+
50
+ private
51
+
52
+ def call(name, args, query)
53
+ @context.public_send(:"#{name}_path", *args, **query)
54
+ end
55
+
56
+ def nested?
57
+ @resource.definition.parent.present?
58
+ end
59
+
60
+ def shallow?
61
+ nested? && @resource.definition.parent.shallow
62
+ end
63
+
64
+ def singular
65
+ @resource.singular_route_key
66
+ end
67
+
68
+ def parent_key
69
+ @resource.parent_resource.singular_route_key
70
+ end
71
+
72
+ def collection_name
73
+ base = @resource.route_key == singular ? "#{singular}_index" : @resource.route_key
74
+ nested? ? "#{parent_key}_#{base}" : base
75
+ end
76
+
77
+ def member_name
78
+ nested? && !shallow? ? "#{parent_key}_#{singular}" : singular
79
+ end
80
+
81
+ def member_args(record)
82
+ nested? && !shallow? ? [parent_of(record), record] : [record]
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Administratum
4
+ # Prepended to each managed model's singleton class. When Ransack is called
5
+ # with auth_object: :administratum, the attributes a resource declares as
6
+ # filters or sortable columns are allowed, so models need no allowlist of
7
+ # their own. Other callers see the model's own allowlist unchanged.
8
+ module Ransackable
9
+ def self.install(model)
10
+ return if model.singleton_class.include?(self)
11
+
12
+ model.singleton_class.prepend(self)
13
+ end
14
+
15
+ def ransackable_attributes(auth_object = nil)
16
+ return super unless auth_object == :administratum
17
+
18
+ Administratum.registry.ransackable_attributes_for(self) | own_allowlist(:ransackable_attributes, auth_object)
19
+ end
20
+
21
+ def ransackable_associations(auth_object = nil)
22
+ return super unless auth_object == :administratum
23
+
24
+ own_allowlist(:ransackable_associations, auth_object)
25
+ end
26
+
27
+ private
28
+
29
+ # The model's own allowlist, if it defines one. Ransack's default either
30
+ # raises (Ransack 4.1+) or, in some 4.x versions, allows every column with a
31
+ # deprecation warning; neither should widen what the admin can search.
32
+ def own_allowlist(name, auth_object)
33
+ definition = method(name).super_method
34
+ return [] if definition.nil? || definition.owner.name.to_s.start_with?("Ransack::")
35
+
36
+ Array(definition.call(auth_object)).map(&:to_s)
37
+ rescue RuntimeError
38
+ []
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Administratum
4
+ # Finds every resource class. Classes are not stored here: they are looked up
5
+ # through Class#descendants after eager loading the resource directories, so
6
+ # code reloading in development needs no bookkeeping.
7
+ class Registry
8
+ def resources
9
+ ensure_loaded!
10
+ Administratum::Resource.descendants
11
+ .select { |klass| live?(klass) && !klass.abstract_resource? && enabled?(klass) }
12
+ .sort_by(&:name)
13
+ .each { |klass| Ransackable.install(klass.definition.model) }
14
+ end
15
+
16
+ def find_by_controller_id(id)
17
+ resources.find { |klass| klass.controller_id == id }
18
+ end
19
+
20
+ def pages
21
+ ensure_loaded!
22
+ Administratum::Page.descendants.select { |klass| live?(klass) && !klass.abstract_page? }.sort_by(&:name)
23
+ end
24
+
25
+ def find_page_by_controller_id(id)
26
+ pages.find { |klass| klass.controller_id == id }
27
+ end
28
+
29
+ def top_level
30
+ resources.reject { |klass| klass.definition.parent }
31
+ end
32
+
33
+ def children_of(resource)
34
+ resources.select { |klass| klass.parent_resource == resource }
35
+ end
36
+
37
+ # The resource that manages `model` at the top level, falling back to any.
38
+ def for_model(model)
39
+ candidates = resources.select { |klass| klass.definition.model == model }
40
+ candidates.find { |klass| klass.definition.parent.nil? } || candidates.first
41
+ end
42
+
43
+ def menu_items
44
+ resources.reject { |klass| klass.definition.menu.hidden || klass.definition.parent || !klass.definition.action_enabled?(:index) }
45
+ .sort_by { |klass| [klass.definition.menu.priority, klass.plural_label] }
46
+ end
47
+
48
+ # Resources and pages for the top navigation, by priority then label.
49
+ def navigation
50
+ (menu_items + pages.reject { |page| page.definition.menu.hidden })
51
+ .sort_by { |klass| [klass.definition.menu.priority, klass.menu_label] }
52
+ end
53
+
54
+ def ransackable_attributes_for(model)
55
+ resources.select { |klass| model <= klass.definition.model }.flat_map { |klass| klass.definition.ransackable_attributes }.uniq
56
+ end
57
+
58
+ def resource_dirs
59
+ dirs = Administratum.config.resource_paths.map { |path| Rails.root.join(path).to_s }
60
+ dirs << Administratum::Engine.root.join("app/admin").to_s
61
+ dirs.select { |dir| Dir.exist?(dir) }
62
+ end
63
+
64
+ def reset!
65
+ @loaded = false
66
+ end
67
+
68
+ private
69
+
70
+ def ensure_loaded!
71
+ return if @loaded
72
+
73
+ @loaded = true
74
+ resource_dirs.each { |dir| load_dir(dir) }
75
+ end
76
+
77
+ # Another admin (ActiveAdmin, say) may own app/admin and take it away from
78
+ # Zeitwerk. Skip such a directory rather than break route loading.
79
+ def load_dir(dir)
80
+ Rails.autoloaders.main.eager_load_dir(dir)
81
+ rescue Zeitwerk::Error => e
82
+ Rails.logger&.warn("[Administratum] Skipping #{dir}: #{e.message}. Point config.resource_paths at a directory " \
83
+ "Rails autoloads, e.g. app/admin_next.")
84
+ end
85
+
86
+ # After a reload, stale classes can linger in descendants until GC.
87
+ def live?(klass)
88
+ klass.name.present? && klass.name.safe_constantize == klass
89
+ end
90
+
91
+ def enabled?(klass)
92
+ Administratum.config.manage_users || klass.name != "Administratum::UserAdmin"
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Administratum
4
+ # Draws one `resources` block per resource class, nesting children under
5
+ # their parent. Each route points at a controller name that
6
+ # Administratum::Resources resolves back to the resource class.
7
+ module Routing
8
+ module_function
9
+
10
+ def draw(router)
11
+ Administratum.registry.reset!
12
+ Administratum.registry.top_level.each { |resource| draw_resource(router, resource) }
13
+ Administratum.registry.pages.each { |page| draw_page(router, page) }
14
+ end
15
+
16
+ # GET /<route_key> plus one route per page_action, named <route_key>_page
17
+ # and <action>_<route_key>_page.
18
+ def draw_page(router, page)
19
+ controller = "pages/#{page.controller_id}"
20
+ key = page.route_key
21
+ router.get key, to: "#{controller}#index", as: "#{key}_page"
22
+ page.definition.custom_actions.each_value do |action|
23
+ router.public_send(action.http_method, "#{key}/#{action.name}", to: "#{controller}##{action.name}", as: "#{action.name}_#{key}_page")
24
+ end
25
+ end
26
+
27
+ def draw_resource(router, resource, nested: false)
28
+ definition = resource.definition
29
+ options = { controller: "resources/#{resource.controller_id}", only: definition.enabled_actions }
30
+ options[:shallow] = true if nested && definition.parent.shallow
31
+
32
+ router.resources(resource.route_key.to_sym, **options) do
33
+ definition.member_actions.each { |action| router.public_send(action.http_method, action.name, on: :member) }
34
+ definition.collection_actions.each { |action| router.public_send(action.http_method, action.name, on: :collection) }
35
+ Administratum.registry.children_of(resource).each { |child| draw_resource(router, child, nested: true) }
36
+ end
37
+ end
38
+ end
39
+
40
+ # Rails resolves "administratum/resources/fantasy_admin" to the constant
41
+ # Administratum::Resources::FantasyAdminController. Instead of defining those
42
+ # constants (which would go stale on reload), look the resource up each time.
43
+ module Resources
44
+ def self.const_missing(name)
45
+ id = name.to_s.delete_suffix("Controller").underscore
46
+ Administratum.registry.find_by_controller_id(id) || super
47
+ end
48
+ end
49
+
50
+ # The same lookup for "administratum/pages/<controller_id>".
51
+ module Pages
52
+ def self.const_missing(name)
53
+ id = name.to_s.delete_suffix("Controller").underscore
54
+ Administratum.registry.find_page_by_controller_id(id) || super
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module Administratum
6
+ # Host Tailwind mode: generates a CSS file with absolute paths into the
7
+ # installed gem, so an app's Tailwind v4 build can include Administratum's
8
+ # tokens, components and class sources. Import it after "tailwindcss" and
9
+ # override any --color-adm-* token in your own @theme.
10
+ module Tailwind
11
+ module_function
12
+
13
+ def root
14
+ File.expand_path("../..", __dir__)
15
+ end
16
+
17
+ def contents
18
+ <<~CSS
19
+ /* Generated by bin/rails administratum:tailwind:sync. Do not edit; do not commit. */
20
+ @import "#{root}/app/assets/tailwind/administratum/theme.css";
21
+ @import "#{root}/app/assets/tailwind/administratum/components.css";
22
+ @source "#{root}/app/views";
23
+ @source "#{root}/app/helpers";
24
+ @source "#{root}/app/assets/javascripts";
25
+ CSS
26
+ end
27
+
28
+ def write(path)
29
+ FileUtils.mkdir_p(File.dirname(path))
30
+ File.write(path, contents)
31
+ path
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Administratum
4
+ VERSION = "0.2.4"
5
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "csv"
4
+ require "devise"
5
+ require "ransack"
6
+ require "pagy"
7
+
8
+ require "administratum/version"
9
+ require "administratum/configuration"
10
+ require "administratum/field_types"
11
+ require "administratum/definition"
12
+ require "administratum/dsl"
13
+ require "administratum/page_dsl"
14
+ require "administratum/paths"
15
+ require "administratum/registry"
16
+ require "administratum/ransackable"
17
+ require "administratum/routing"
18
+ require "administratum/engine"
19
+
20
+ module Administratum
21
+ class Error < StandardError; end
22
+
23
+ def self.config
24
+ @config ||= Configuration.new
25
+ end
26
+
27
+ def self.configure
28
+ yield config
29
+ end
30
+
31
+ def self.registry
32
+ @registry ||= Registry.new
33
+ end
34
+
35
+ def self.field_types
36
+ config.field_types
37
+ end
38
+
39
+ # Used by tests to start from a clean configuration.
40
+ def self.reset_config!
41
+ @config = Configuration.new
42
+ end
43
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module Administratum
6
+ module Generators
7
+ # rails g administratum:install
8
+ class InstallGenerator < Rails::Generators::Base
9
+ source_root File.expand_path("templates", __dir__)
10
+
11
+ class_option :path, type: :string, default: "/administratum", desc: "Where to mount the admin"
12
+
13
+ def copy_initializer
14
+ template "administratum.rb", "config/initializers/administratum.rb"
15
+ end
16
+
17
+ def create_admin_directory
18
+ empty_directory "app/admin"
19
+ create_file "app/admin/.keep", ""
20
+ end
21
+
22
+ def copy_migrations
23
+ rake "administratum:install:migrations"
24
+ end
25
+
26
+ def mount_engine
27
+ route %(mount Administratum::Engine => "#{options[:path]}")
28
+ end
29
+
30
+ def show_next_steps
31
+ say <<~TEXT
32
+
33
+ Administratum is installed. Next:
34
+ 1. bin/rails db:migrate
35
+ 2. bin/rails administratum:create_user EMAIL=you@example.com
36
+ 3. bin/rails g administratum:resource YourModel
37
+ 4. Visit #{options[:path]}
38
+ TEXT
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ Administratum.configure do |config|
4
+ # Shown in the top bar and the browser title.
5
+ config.app_name = "<%= Rails.application.class.module_parent_name.titleize %> Admin"
6
+
7
+ # config.logo = "admin/logo.svg"
8
+ # config.mailer_sender = "admin@example.com"
9
+ # config.per_page = 25
10
+
11
+ # Controller the admin inherits from. Use it for app-wide before_actions.
12
+ # config.parent_controller = "ActionController::Base"
13
+
14
+ # Partial rendered on the dashboard instead of the resource overview.
15
+ # config.dashboard = "admin/dashboard"
16
+
17
+ # Load your own JS (islands, Turbo, Stimulus…) and CSS into the admin <head>.
18
+ # config.head = ->(view) { view.javascript_importmap_tags("admin") }
19
+ # config.head = ->(view) { view.vite_javascript_tag("admin") }
20
+ # config.head_partial = "admin/head"
21
+
22
+ # Colour tokens (--color-adm-*). Applied on top of the bundled stylesheet.
23
+ # config.theme = { primary: "#7c3aed" }
24
+ # config.dark_theme = { primary: "#a78bfa" }
25
+
26
+ # :bundled uses the stylesheet shipped with the gem; :host means your
27
+ # Tailwind v4 build includes Administratum (see bin/rails administratum:tailwind:sync).
28
+ # config.stylesheet = :bundled
29
+
30
+ # Custom field types: a partial that receives form:, field:, resource:, value:.
31
+ # config.field_types.register :tags, partial: "admin/fields/tags"
32
+
33
+ # Extra helper modules for admin views.
34
+ # config.helpers = [ApplicationHelper]
35
+
36
+ # Resource templates are looked up in app/views/<view_prefix>/<resource>.
37
+ # config.view_prefix = "admin"
38
+
39
+ # Set to false to hide admin user management.
40
+ # config.manage_users = true
41
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module Administratum
6
+ module Generators
7
+ # rails g administratum:resource Post
8
+ class ResourceGenerator < Rails::Generators::NamedBase
9
+ source_root File.expand_path("templates", __dir__)
10
+
11
+ SKIPPED_COLUMNS = %w[id created_at updated_at].freeze
12
+
13
+ def create_resource_file
14
+ template "resource.rb", File.join("app/admin", class_path, "#{file_name}_admin.rb")
15
+ end
16
+
17
+ private
18
+
19
+ def model_class
20
+ class_name.safe_constantize
21
+ end
22
+
23
+ def field_names
24
+ return [] unless model_class&.table_exists?
25
+
26
+ model_class.column_names - SKIPPED_COLUMNS - %w[encrypted_password password_digest]
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ class <%= class_name %>Admin < Administratum::Resource
4
+ # filters :name
5
+
6
+ # index do
7
+ # column :name
8
+ # column :created_at
9
+ # end
10
+
11
+ form do
12
+ <% if field_names.any? -%>
13
+ <% field_names.each do |name| -%>
14
+ field :<%= name %>
15
+ <% end -%>
16
+ <% else -%>
17
+ # field :name
18
+ <% end -%>
19
+ end
20
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :administratum do
4
+ desc "Create an admin user: EMAIL=you@example.com [PASSWORD=secret]"
5
+ task create_user: :environment do
6
+ email = ENV.fetch("EMAIL") { abort "Set EMAIL=you@example.com" }
7
+ password = ENV["PASSWORD"].presence || SecureRandom.base58(16)
8
+ user = Administratum::User.create!(email:, password:, password_confirmation: password)
9
+ puts "Created #{user.email}"
10
+ puts "Password: #{password}" if ENV["PASSWORD"].blank?
11
+ end
12
+
13
+ namespace :tailwind do
14
+ desc "Write the import file that adds Administratum to your own Tailwind v4 build"
15
+ task :sync do
16
+ require "administratum/tailwind"
17
+ path = Administratum::Tailwind.write(ENV.fetch("OUTPUT", "app/assets/tailwind/administratum.gen.css"))
18
+ puts "Wrote #{path}"
19
+ end
20
+ end
21
+ end
22
+
23
+ # Keep the generated import file current wherever assets are built.
24
+ # Declaring the prerequisite (rather than looking the task up) works whichever
25
+ # order the rake files load in.
26
+ # The generated file is gitignored, so on a fresh checkout it only exists after
27
+ # a sync; also hook in when the initializer opts into host mode.
28
+ host_mode = File.exist?("config/initializers/administratum.rb") &&
29
+ File.read("config/initializers/administratum.rb").match?(/^\s*config\.stylesheet\s*=\s*:host/)
30
+ if host_mode || File.exist?("app/assets/tailwind/administratum.gen.css")
31
+ %w[assets:precompile vite:build].each { |name| task(name).enhance(["administratum:tailwind:sync"]) }
32
+ end