js_admin 0.0.1 → 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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +17 -0
  3. data/README.md +179 -19
  4. data/app/assets/javascripts/js_admin/application.js +13 -0
  5. data/app/assets/javascripts/js_admin/controllers/flash_controller.js +23 -0
  6. data/app/assets/javascripts/js_admin/controllers/theme_controller.js +62 -0
  7. data/app/assets/tailwind/js_admin/engine.css +4 -0
  8. data/app/controllers/js_admin/application_controller.rb +28 -0
  9. data/app/controllers/js_admin/home_controller.rb +6 -0
  10. data/app/controllers/js_admin/resources_controller.rb +17 -4
  11. data/app/helpers/js_admin/application_helper.rb +286 -19
  12. data/app/views/js_admin/home/index.html.erb +5 -0
  13. data/app/views/js_admin/models/_index_content.html.erb +20 -0
  14. data/app/views/js_admin/models/index.html.erb +3 -20
  15. data/app/views/js_admin/resources/_edit_content.html.erb +11 -0
  16. data/app/views/js_admin/resources/_form.html.erb +20 -13
  17. data/app/views/js_admin/resources/_index_content.html.erb +74 -0
  18. data/app/views/js_admin/resources/_new_content.html.erb +10 -0
  19. data/app/views/js_admin/resources/_show_content.html.erb +26 -0
  20. data/app/views/js_admin/resources/destroy.turbo_stream.erb +7 -0
  21. data/app/views/js_admin/resources/edit.html.erb +3 -11
  22. data/app/views/js_admin/resources/index.html.erb +3 -52
  23. data/app/views/js_admin/resources/new.html.erb +3 -10
  24. data/app/views/js_admin/resources/show.html.erb +3 -26
  25. data/app/views/layouts/js_admin/_content_frame.html.erb +7 -0
  26. data/app/views/layouts/js_admin/_flash.html.erb +3 -0
  27. data/app/views/layouts/js_admin/_flash_messages.html.erb +9 -0
  28. data/app/views/layouts/js_admin/_sidebar.html.erb +36 -0
  29. data/app/views/layouts/js_admin/application.html.erb +19 -17
  30. data/config/routes.rb +2 -1
  31. data/lib/generators/js_admin/install/install_generator.rb +63 -5
  32. data/lib/js_admin/configuration.rb +16 -3
  33. data/lib/js_admin/field.rb +4 -4
  34. data/lib/js_admin/resource.rb +12 -4
  35. data/lib/js_admin/version.rb +1 -1
  36. data/lib/js_admin.rb +6 -0
  37. metadata +37 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b816de43bee7878c7eccf7acb2a158b304cbfbbb80bd65566329303fea76c0e2
4
- data.tar.gz: 5c760edf388ea5dfc66985197ca48f99d79cfd4af9923635c98e4d93217856fc
3
+ metadata.gz: e58cee8e95f86b47a5487366c9bb11691c14ad57da5a8fbb5d9f3522b49c7372
4
+ data.tar.gz: e47f66d3d176de880706e7fd52deb86d03ba058bf63bc75b9d702ba4e20f0814
5
5
  SHA512:
6
- metadata.gz: 753fd6b7de19253fc292b4f32654231cda74211c131a07080128cdd3290b9acd8229b3c7f320c5bb32a7248113bff3b04062cf4bbcfc1d4b5d39bcbbd6de57a3
7
- data.tar.gz: 1b26bb7126e1404f1bd0d85f8ab6693eee2b94c029e9c0e4c0a5775d7b921af3e0d68407b22ba7c3e7c41fbb17e63852f6b83a43a9bc24972f46798a906279b0
6
+ metadata.gz: cd4ee8893dea3b7a19998cf43923919491a6b5fbbda3865b4efb2b127b7d3e851da0d790a9e3be30dceaa84e9b97990b1672c13db3a9c7621f484091f3f07ad4
7
+ data.tar.gz: 1c149b1cca178d1850fc1a78051d1b59af102d3323a4c38f57c93fd3952652cd37c1e0657bd65da3973b81ab3aaaf0f0da94ff6517e14d8e5846202c47df0d10
data/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ## 0.1.0 - 2026-07-18
6
+
7
+ - Change the default mount path to `/admin`.
8
+ - Add optional HTTP Basic Auth configuration.
9
+ - Generate a commented `config/initializers/js_admin.rb` during installation.
10
+ - Pin JSAdmin JavaScript through `importmap-rails` during installation.
11
+ - Add sidebar model navigation with namespace grouping.
12
+ - Use Turbo Frames, Turbo Streams, and Stimulus controllers for the admin shell.
13
+ - Add fixed header/sidebar layout, dark mode support, and improved Tailwind UI spacing.
14
+ - Add compact pagination controls and set the default page size to 10 records.
15
+ - Add `View`, `Edit`, and `Delete` actions to resource tables.
16
+ - Render form validation errors inline without Rails `field_with_errors` layout wrappers.
17
+ - Add sandbox setup for browser testing with richer demo models and seed data.
18
+ - Add AGENTS/CLAUDE project guidance files for LLM-assisted development.
19
+
3
20
  ## 0.0.1 - 2026-07-12
4
21
 
5
22
  - Initial release for RubyGems publishing test.
data/README.md CHANGED
@@ -1,19 +1,20 @@
1
1
  # JSAdmin
2
2
 
3
- JSAdmin is a mountable Rails engine that exposes Active Record models through
4
- a generic CRUD UI. It discovers models from the host Rails application and builds
3
+ JSAdmin is a mountable Rails engine that exposes Active Record models through a
4
+ generic CRUD UI. It discovers models from the host Rails application and builds
5
5
  forms from database column metadata.
6
6
 
7
- > 2026-07-12 기준 RubyGems API에서 `js_admin`은 등록되지 않은 이름으로 확인했습니다. 공개 배포 직전에는 다시 한 번 점유 여부를 확인하세요.
7
+ It is built for Rails 8.1+, Tailwind CSS, and Hotwire-compatible Rails helpers.
8
8
 
9
9
  ## Installation
10
+
10
11
  Add this line to your application's Gemfile:
11
12
 
12
13
  ```ruby
13
- gem "js_admin", path: "../js_admin"
14
+ gem "js_admin"
14
15
  ```
15
16
 
16
- And then execute:
17
+ Install and mount the engine:
17
18
 
18
19
  ```bash
19
20
  bundle install
@@ -21,31 +22,91 @@ bin/rails generate js_admin:install
21
22
  bin/rails tailwindcss:engines
22
23
  ```
23
24
 
24
- The install generator mounts the engine at `/js_admin` and imports the engine
25
- Tailwind entrypoint when `app/assets/tailwind/application.css` exists.
25
+ The install generator mounts JSAdmin at `/admin`, pins JSAdmin's JavaScript in
26
+ `config/importmap.rb`, and imports the engine Tailwind entrypoint when
27
+ `app/assets/tailwind/application.css` exists. You can choose another mount path:
28
+
29
+ ```bash
30
+ bin/rails generate js_admin:install --path=/backoffice
31
+ bin/rails tailwindcss:engines
32
+ ```
33
+
34
+ JSAdmin uses `importmap-rails` for JavaScript loading. Rails 8 default apps
35
+ already include `config/importmap.rb`; if it was removed, run
36
+ `bin/rails importmap:install` before running `js_admin:install`.
37
+
38
+ The generator adds these pins when they are missing:
39
+
40
+ ```ruby
41
+ pin "@hotwired/turbo-rails", to: "turbo.min.js"
42
+ pin "@hotwired/stimulus", to: "stimulus.min.js"
43
+ pin "js_admin", to: "js_admin/application.js", preload: true
44
+ pin "js_admin/controllers/theme_controller", to: "js_admin/controllers/theme_controller.js", preload: true
45
+ pin "js_admin/controllers/flash_controller", to: "js_admin/controllers/flash_controller.js", preload: true
46
+ ```
26
47
 
27
48
  ## Usage
28
49
 
29
- Open:
50
+ Open the mounted path:
30
51
 
31
52
  ```text
32
- /js_admin
53
+ /admin
33
54
  ```
34
55
 
35
- By default, JSAdmin lists every concrete Active Record model with an existing
36
- database table. No per-model field setup is required.
56
+ The home page is intentionally empty. Model navigation lives in the sidebar.
57
+ By default, JSAdmin exposes every concrete Active Record model with an existing
58
+ database table. Namespaced models are grouped under namespace dropdowns. No
59
+ per-model field setup is required.
60
+
61
+ The UI includes a fixed header, fixed desktop sidebar, dark mode toggle, compact
62
+ pagination controls, and inline form validation errors. The default page size is
63
+ 10 records.
64
+
65
+ The install generator also creates `config/initializers/js_admin.rb` with
66
+ commented configuration examples. JSAdmin works without changing that file.
67
+
68
+ Available configuration options:
69
+
70
+ | Option | Default | Description |
71
+ | --- | --- | --- |
72
+ | `included_models` | `[]` | Model names/classes to expose. Empty means every discovered concrete Active Record model. |
73
+ | `excluded_models` | `[]` | Model names/classes to hide after discovery. |
74
+ | `records_per_page` | `10` | Number of records shown per resource index page. |
75
+ | `association_limit` | `100` | Maximum number of associated records loaded into a `belongs_to` select. |
76
+ | `display_name_methods` | `%i[name title email username id]` | Methods tried in order when displaying records in tables and select labels. |
77
+ | `basic_auth_username` | `nil` | HTTP Basic Auth username. Auth is disabled unless username and password are both present. |
78
+ | `basic_auth_password` | `nil` | HTTP Basic Auth password. |
79
+ | `basic_auth_realm` | `"JSAdmin"` | HTTP Basic Auth realm. |
37
80
 
38
- Optional model filtering:
81
+ Example:
39
82
 
40
83
  ```ruby
41
84
  # config/initializers/js_admin.rb
42
85
  JSAdmin.configure do |config|
43
86
  config.include_models User, Post
44
87
  config.exclude_models AuditLog
45
- config.records_per_page = 50
88
+ config.records_per_page = 10
46
89
  end
47
90
  ```
48
91
 
92
+ ## Authentication
93
+
94
+ JSAdmin works without authentication unless Basic Auth credentials are
95
+ configured. When both username and password are set, every JSAdmin request
96
+ requires HTTP Basic Auth.
97
+
98
+ ```ruby
99
+ # config/initializers/js_admin.rb
100
+ JSAdmin.configure do |config|
101
+ config.basic_auth_username = ENV["JS_ADMIN_BASIC_AUTH_USERNAME"]
102
+ config.basic_auth_password = ENV["JS_ADMIN_BASIC_AUTH_PASSWORD"]
103
+ config.basic_auth_realm = "JSAdmin"
104
+ end
105
+ ```
106
+
107
+ For production apps, you can also mount JSAdmin inside the host application's
108
+ admin authentication constraint when you need app-level authorization.
109
+
49
110
  ## Field Mapping
50
111
 
51
112
  JSAdmin maps database columns to inputs automatically:
@@ -63,14 +124,113 @@ JSAdmin maps database columns to inputs automatically:
63
124
  Primary keys, timestamps, STI inheritance columns, readonly attributes, and
64
125
  generated columns are not editable.
65
126
 
66
- ## Security
127
+ ## Hotwire Scope
128
+
129
+ JSAdmin depends on `importmap-rails`, `turbo-rails`, and `stimulus-rails` so it
130
+ works naturally in modern Rails apps using the Rails default Hotwire setup.
131
+
132
+ The admin shell keeps the header and sidebar stable while the main CRUD area is
133
+ wrapped in a Turbo Frame. Sidebar navigation, resource links, forms, and
134
+ pagination update that frame instead of replacing the whole page. Delete actions
135
+ can respond with Turbo Streams to refresh the flash region and resource list.
136
+
137
+ Stimulus controllers manage the dark mode toggle and auto-dismiss flash
138
+ messages. The selected theme is persisted in storage and a cookie so the engine
139
+ layout can render the initial dark class without inline JavaScript.
140
+
141
+ ## Validation Errors
142
+
143
+ Form validation errors are rendered inside the form next to the matching field.
144
+ JSAdmin renders form controls directly so Rails' default `field_with_errors`
145
+ wrapper does not break the Tailwind grid layout. Errors that do not belong to an
146
+ editable field are shown at the top of the form.
147
+
148
+ ## Development
149
+
150
+ Run the test suite:
151
+
152
+ ```bash
153
+ bundle install
154
+ bundle exec rails test
155
+ ```
156
+
157
+ Build the gem locally:
158
+
159
+ ```bash
160
+ gem build js_admin.gemspec
161
+ ```
162
+
163
+ ### Dummy App
164
+
165
+ This repository includes `test/dummy` as the Rails engine test app. It is used by
166
+ the test suite and should stay small.
167
+
168
+ You can run it manually for functional checks:
169
+
170
+ ```bash
171
+ bin/rails db:prepare
172
+ bin/rails server
173
+ ```
174
+
175
+ Open:
176
+
177
+ ```text
178
+ http://localhost:3000/admin
179
+ ```
180
+
181
+ ### Sandbox App
182
+
183
+ Use `sandbox/` when you want to verify the real installation flow in a fresh
184
+ Rails app. The sandbox directory is ignored by Git.
185
+
186
+ Create a sandbox app wired to this local checkout:
187
+
188
+ ```bash
189
+ bin/setup_sandbox
190
+ ```
191
+
192
+ The setup script creates several demo models, including a namespaced model, seeds
193
+ sample records, and runs an initial Tailwind build.
67
194
 
68
- Zero model configuration is convenient, but it is dangerous to expose without
69
- authentication. In production, mount the engine inside your app's admin
70
- authentication constraint.
195
+ Run a one-off browser check:
71
196
 
72
- ## Contributing
73
- Contribution directions go here.
197
+ ```bash
198
+ cd sandbox
199
+ bin/rails server
200
+ ```
201
+
202
+ For iterative UI/CSS development, run the Rails server and Tailwind watcher
203
+ together:
204
+
205
+ ```bash
206
+ cd sandbox
207
+ bin/dev
208
+ ```
209
+
210
+ Open:
211
+
212
+ ```text
213
+ http://localhost:3000/admin
214
+ ```
215
+
216
+ Rebuild it from scratch:
217
+
218
+ ```bash
219
+ rm -rf sandbox
220
+ bin/setup_sandbox
221
+ ```
222
+
223
+ ## Release
224
+
225
+ Releases are published to RubyGems through GitHub Actions and RubyGems Trusted
226
+ Publishing. To release a new version, update `lib/js_admin/version.rb`, update
227
+ `CHANGELOG.md`, and push a matching tag:
228
+
229
+ ```bash
230
+ git tag v0.0.2
231
+ git push origin v0.0.2
232
+ ```
74
233
 
75
234
  ## License
235
+
76
236
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,13 @@
1
+ import "@hotwired/turbo-rails"
2
+ import { Application } from "@hotwired/stimulus"
3
+ import ThemeController from "js_admin/controllers/theme_controller"
4
+ import FlashController from "js_admin/controllers/flash_controller"
5
+
6
+ const application = window.Stimulus || window.JSAdminStimulus || Application.start()
7
+
8
+ if (!window.Stimulus) {
9
+ window.JSAdminStimulus = application
10
+ }
11
+
12
+ application.register("js-admin-theme", ThemeController)
13
+ application.register("js-admin-flash", FlashController)
@@ -0,0 +1,23 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static values = {
5
+ timeout: { type: Number, default: 4000 }
6
+ }
7
+
8
+ connect() {
9
+ this.timeoutId = window.setTimeout(() => this.dismiss(), this.timeoutValue)
10
+ }
11
+
12
+ disconnect() {
13
+ window.clearTimeout(this.timeoutId)
14
+ }
15
+
16
+ dismiss() {
17
+ this.element.classList.add("opacity-0", "translate-y-1")
18
+
19
+ window.setTimeout(() => {
20
+ this.element.remove()
21
+ }, 300)
22
+ }
23
+ }
@@ -0,0 +1,62 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ const THEME_STORAGE_KEY = "js_admin_theme"
4
+ const THEME_COOKIE_NAME = "js_admin_theme"
5
+ const THEME_COOKIE_MAX_AGE = 60 * 60 * 24 * 365
6
+
7
+ export default class extends Controller {
8
+ static targets = ["toggle"]
9
+
10
+ connect() {
11
+ this.applyPreferredTheme()
12
+ this.syncToggle()
13
+ }
14
+
15
+ toggle() {
16
+ const enabled = !document.documentElement.classList.contains("dark")
17
+
18
+ document.documentElement.classList.toggle("dark", enabled)
19
+ this.persistTheme(enabled)
20
+ this.syncToggle()
21
+ }
22
+
23
+ syncToggle() {
24
+ if (!this.hasToggleTarget) return
25
+
26
+ this.toggleTarget.setAttribute("aria-pressed", document.documentElement.classList.contains("dark"))
27
+ }
28
+
29
+ persistTheme(enabled) {
30
+ const theme = enabled ? "dark" : "light"
31
+
32
+ try {
33
+ localStorage.setItem(THEME_STORAGE_KEY, theme)
34
+ } catch (_error) {
35
+ }
36
+
37
+ document.cookie = `${THEME_COOKIE_NAME}=${theme}; path=/; max-age=${THEME_COOKIE_MAX_AGE}; SameSite=Lax`
38
+ }
39
+
40
+ applyPreferredTheme() {
41
+ const storedTheme = this.storedTheme()
42
+ const darkEnabled = storedTheme === "dark" || (!storedTheme && window.matchMedia("(prefers-color-scheme: dark)").matches)
43
+
44
+ document.documentElement.classList.toggle("dark", darkEnabled)
45
+ }
46
+
47
+ storedTheme() {
48
+ try {
49
+ return localStorage.getItem(THEME_STORAGE_KEY) || this.cookieTheme()
50
+ } catch (_error) {
51
+ return this.cookieTheme()
52
+ }
53
+ }
54
+
55
+ cookieTheme() {
56
+ const themeCookie = document.cookie
57
+ .split("; ")
58
+ .find((cookie) => cookie.startsWith(`${THEME_COOKIE_NAME}=`))
59
+
60
+ return themeCookie?.split("=")[1]
61
+ }
62
+ }
@@ -1,4 +1,8 @@
1
1
  @import "tailwindcss";
2
2
 
3
+ @custom-variant dark (&:where(.dark, .dark *));
4
+
5
+ @source "../../../views/layouts/js_admin/**/*.erb";
3
6
  @source "../../../views/js_admin/**/*.erb";
4
7
  @source "../../../helpers/js_admin/**/*.rb";
8
+ @source "../../../assets/javascripts/js_admin/**/*.js";
@@ -1,4 +1,32 @@
1
+ require "digest"
2
+
1
3
  module JSAdmin
2
4
  class ApplicationController < ActionController::Base
5
+ before_action :authenticate_basic_auth
6
+
7
+ private
8
+
9
+ def authenticate_basic_auth
10
+ return unless JSAdmin.configuration.basic_auth_enabled?
11
+
12
+ authenticate_or_request_with_http_basic(basic_auth_realm) do |username, password|
13
+ secure_compare(username, JSAdmin.configuration.basic_auth_username) &&
14
+ secure_compare(password, JSAdmin.configuration.basic_auth_password)
15
+ end
16
+ end
17
+
18
+ def basic_auth_realm
19
+ realm = JSAdmin.configuration.basic_auth_realm.to_s
20
+ return "JSAdmin" if realm.empty?
21
+
22
+ realm
23
+ end
24
+
25
+ def secure_compare(provided_value, expected_value)
26
+ ActiveSupport::SecurityUtils.secure_compare(
27
+ Digest::SHA256.hexdigest(provided_value.to_s),
28
+ Digest::SHA256.hexdigest(expected_value.to_s)
29
+ )
30
+ end
3
31
  end
4
32
  end
@@ -0,0 +1,6 @@
1
+ module JSAdmin
2
+ class HomeController < ApplicationController
3
+ def index
4
+ end
5
+ end
6
+ end
@@ -4,9 +4,7 @@ module JSAdmin
4
4
  before_action :set_record, only: %i[show edit update destroy]
5
5
 
6
6
  def index
7
- @page = [ params.fetch(:page, 1).to_i, 1 ].max
8
- @records = @resource.records(page: @page)
9
- @total_count = @resource.count
7
+ load_records
10
8
  end
11
9
 
12
10
  def show
@@ -39,7 +37,15 @@ module JSAdmin
39
37
 
40
38
  def destroy
41
39
  @record.destroy
42
- redirect_to resources_path(@resource.model_id), notice: "#{@resource.singular_label} was deleted."
40
+ message = "#{@resource.singular_label} was deleted."
41
+
42
+ respond_to do |format|
43
+ format.turbo_stream do
44
+ flash.now[:notice] = message
45
+ load_records
46
+ end
47
+ format.html { redirect_to resources_path(@resource.model_id), notice: message }
48
+ end
43
49
  end
44
50
 
45
51
  private
@@ -52,6 +58,13 @@ module JSAdmin
52
58
  @record = @resource.find(params[:id])
53
59
  end
54
60
 
61
+ def load_records
62
+ @total_count = @resource.count
63
+ @total_pages = @resource.total_pages(total_count: @total_count)
64
+ @page = [ [ params.fetch(:page, 1).to_i, 1 ].max, @total_pages ].min
65
+ @records = @resource.records(page: @page)
66
+ end
67
+
55
68
  def resource_params
56
69
  params
57
70
  .fetch(@resource.param_key, ActionController::Parameters.new)