js_admin 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/MIT-LICENSE +20 -0
- data/README.md +76 -0
- data/Rakefile +6 -0
- data/app/assets/stylesheets/js_admin/application.css +15 -0
- data/app/assets/tailwind/js_admin/engine.css +4 -0
- data/app/controllers/js_admin/application_controller.rb +4 -0
- data/app/controllers/js_admin/models_controller.rb +7 -0
- data/app/controllers/js_admin/resources_controller.rb +61 -0
- data/app/helpers/js_admin/application_helper.rb +67 -0
- data/app/models/js_admin/application_record.rb +5 -0
- data/app/views/js_admin/models/index.html.erb +22 -0
- data/app/views/js_admin/resources/_form.html.erb +30 -0
- data/app/views/js_admin/resources/edit.html.erb +13 -0
- data/app/views/js_admin/resources/index.html.erb +54 -0
- data/app/views/js_admin/resources/new.html.erb +12 -0
- data/app/views/js_admin/resources/show.html.erb +28 -0
- data/app/views/layouts/js_admin/application.html.erb +37 -0
- data/config/routes.rb +12 -0
- data/lib/generators/js_admin/install/install_generator.rb +24 -0
- data/lib/js_admin/configuration.rb +28 -0
- data/lib/js_admin/engine.rb +15 -0
- data/lib/js_admin/field.rb +125 -0
- data/lib/js_admin/registry.rb +58 -0
- data/lib/js_admin/resource.rb +88 -0
- data/lib/js_admin/version.rb +3 -0
- data/lib/js_admin.rb +27 -0
- data/lib/tasks/js_admin_tasks.rake +4 -0
- metadata +152 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: b816de43bee7878c7eccf7acb2a158b304cbfbbb80bd65566329303fea76c0e2
|
|
4
|
+
data.tar.gz: 5c760edf388ea5dfc66985197ca48f99d79cfd4af9923635c98e4d93217856fc
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 753fd6b7de19253fc292b4f32654231cda74211c131a07080128cdd3290b9acd8229b3c7f320c5bb32a7248113bff3b04062cf4bbcfc1d4b5d39bcbbd6de57a3
|
|
7
|
+
data.tar.gz: 1b26bb7126e1404f1bd0d85f8ab6693eee2b94c029e9c0e4c0a5775d7b921af3e0d68407b22ba7c3e7c41fbb17e63852f6b83a43a9bc24972f46798a906279b0
|
data/CHANGELOG.md
ADDED
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright TODO: Write your name
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# JSAdmin
|
|
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
|
|
5
|
+
forms from database column metadata.
|
|
6
|
+
|
|
7
|
+
> 2026-07-12 기준 RubyGems API에서 `js_admin`은 등록되지 않은 이름으로 확인했습니다. 공개 배포 직전에는 다시 한 번 점유 여부를 확인하세요.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
Add this line to your application's Gemfile:
|
|
11
|
+
|
|
12
|
+
```ruby
|
|
13
|
+
gem "js_admin", path: "../js_admin"
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
And then execute:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
bundle install
|
|
20
|
+
bin/rails generate js_admin:install
|
|
21
|
+
bin/rails tailwindcss:engines
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The install generator mounts the engine at `/js_admin` and imports the engine
|
|
25
|
+
Tailwind entrypoint when `app/assets/tailwind/application.css` exists.
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
Open:
|
|
30
|
+
|
|
31
|
+
```text
|
|
32
|
+
/js_admin
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
By default, JSAdmin lists every concrete Active Record model with an existing
|
|
36
|
+
database table. No per-model field setup is required.
|
|
37
|
+
|
|
38
|
+
Optional model filtering:
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
# config/initializers/js_admin.rb
|
|
42
|
+
JSAdmin.configure do |config|
|
|
43
|
+
config.include_models User, Post
|
|
44
|
+
config.exclude_models AuditLog
|
|
45
|
+
config.records_per_page = 50
|
|
46
|
+
end
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Field Mapping
|
|
50
|
+
|
|
51
|
+
JSAdmin maps database columns to inputs automatically:
|
|
52
|
+
|
|
53
|
+
| Column metadata | Input |
|
|
54
|
+
| --- | --- |
|
|
55
|
+
| `text` | `textarea` |
|
|
56
|
+
| `boolean` | checkbox |
|
|
57
|
+
| `integer`, `bigint`, `float`, `decimal` | number input |
|
|
58
|
+
| `date`, `datetime`, `time` | matching date/time input |
|
|
59
|
+
| Rails enum | select |
|
|
60
|
+
| `belongs_to` foreign key | select |
|
|
61
|
+
| other columns | text input |
|
|
62
|
+
|
|
63
|
+
Primary keys, timestamps, STI inheritance columns, readonly attributes, and
|
|
64
|
+
generated columns are not editable.
|
|
65
|
+
|
|
66
|
+
## Security
|
|
67
|
+
|
|
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.
|
|
71
|
+
|
|
72
|
+
## Contributing
|
|
73
|
+
Contribution directions go here.
|
|
74
|
+
|
|
75
|
+
## License
|
|
76
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
|
3
|
+
* listed below.
|
|
4
|
+
*
|
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
|
6
|
+
* or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
|
|
7
|
+
*
|
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
|
9
|
+
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
|
|
10
|
+
* files in this directory. Styles in this file should be added after the last require_* statement.
|
|
11
|
+
* It is generally better to create a new file per style scope.
|
|
12
|
+
*
|
|
13
|
+
*= require_tree .
|
|
14
|
+
*= require_self
|
|
15
|
+
*/
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
module JSAdmin
|
|
2
|
+
class ResourcesController < ApplicationController
|
|
3
|
+
before_action :set_resource
|
|
4
|
+
before_action :set_record, only: %i[show edit update destroy]
|
|
5
|
+
|
|
6
|
+
def index
|
|
7
|
+
@page = [ params.fetch(:page, 1).to_i, 1 ].max
|
|
8
|
+
@records = @resource.records(page: @page)
|
|
9
|
+
@total_count = @resource.count
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def show
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def new
|
|
16
|
+
@record = @resource.new_record
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def create
|
|
20
|
+
@record = @resource.new_record(resource_params)
|
|
21
|
+
|
|
22
|
+
if @record.save
|
|
23
|
+
redirect_to resource_path(@resource.model_id, @record), notice: "#{@resource.singular_label} was created."
|
|
24
|
+
else
|
|
25
|
+
render :new, status: :unprocessable_entity
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def edit
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def update
|
|
33
|
+
if @record.update(resource_params)
|
|
34
|
+
redirect_to resource_path(@resource.model_id, @record), notice: "#{@resource.singular_label} was updated."
|
|
35
|
+
else
|
|
36
|
+
render :edit, status: :unprocessable_entity
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def destroy
|
|
41
|
+
@record.destroy
|
|
42
|
+
redirect_to resources_path(@resource.model_id), notice: "#{@resource.singular_label} was deleted."
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def set_resource
|
|
48
|
+
@resource = JSAdmin.resource_for!(params[:model_id])
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def set_record
|
|
52
|
+
@record = @resource.find(params[:id])
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def resource_params
|
|
56
|
+
params
|
|
57
|
+
.fetch(@resource.param_key, ActionController::Parameters.new)
|
|
58
|
+
.permit(*@resource.editable_fields.map(&:name))
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
module JSAdmin
|
|
2
|
+
module ApplicationHelper
|
|
3
|
+
TEXT_INPUT_CLASSES = "w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm text-slate-950 shadow-sm focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-200"
|
|
4
|
+
SELECT_INPUT_CLASSES = "w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm text-slate-950 shadow-sm focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-200"
|
|
5
|
+
CHECKBOX_INPUT_CLASSES = "h-4 w-4 rounded border-slate-300 text-sky-600 focus:ring-sky-500"
|
|
6
|
+
|
|
7
|
+
def js_admin_field_input(form, field)
|
|
8
|
+
case field.input_kind
|
|
9
|
+
when :text_area
|
|
10
|
+
form.text_area(field.name, rows: 5, class: TEXT_INPUT_CLASSES)
|
|
11
|
+
when :check_box
|
|
12
|
+
tag.div(class: "flex h-10 items-center") do
|
|
13
|
+
form.check_box(field.name, class: CHECKBOX_INPUT_CLASSES)
|
|
14
|
+
end
|
|
15
|
+
when :number_field
|
|
16
|
+
form.number_field(field.name, step: field.number_step, class: TEXT_INPUT_CLASSES)
|
|
17
|
+
when :date_field
|
|
18
|
+
form.date_field(field.name, class: TEXT_INPUT_CLASSES)
|
|
19
|
+
when :datetime_field
|
|
20
|
+
form.datetime_field(field.name, class: TEXT_INPUT_CLASSES)
|
|
21
|
+
when :time_field
|
|
22
|
+
form.time_field(field.name, class: TEXT_INPUT_CLASSES)
|
|
23
|
+
when :enum_select
|
|
24
|
+
form.select(field.name, field.enum_values, { include_blank: true }, class: SELECT_INPUT_CLASSES)
|
|
25
|
+
when :association_select
|
|
26
|
+
form.select(field.name, field.association_options, { include_blank: true }, class: SELECT_INPUT_CLASSES)
|
|
27
|
+
else
|
|
28
|
+
form.text_field(field.name, class: TEXT_INPUT_CLASSES)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def js_admin_table_value(field, record)
|
|
33
|
+
value = field.formatted_value(record)
|
|
34
|
+
classes = value == "NULL" ? "text-slate-400" : "text-slate-700"
|
|
35
|
+
|
|
36
|
+
tag.span(truncate(value, length: 80), class: classes)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def js_admin_stylesheet_link_tag(logical_name)
|
|
40
|
+
return unless js_admin_asset_exists?("#{logical_name}.css")
|
|
41
|
+
|
|
42
|
+
stylesheet_link_tag logical_name, "data-turbo-track": "reload"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def js_admin_hotwire_javascript_tags
|
|
46
|
+
if respond_to?(:javascript_importmap_tags) && Rails.root.join("config/importmap.rb").exist?
|
|
47
|
+
javascript_importmap_tags
|
|
48
|
+
elsif js_admin_asset_exists?("application.js")
|
|
49
|
+
javascript_include_tag "application", type: "module", "data-turbo-track": "reload"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def js_admin_asset_exists?(logical_path)
|
|
54
|
+
assets = Rails.application.assets
|
|
55
|
+
|
|
56
|
+
if assets.respond_to?(:load_path)
|
|
57
|
+
assets.load_path.find(logical_path).present?
|
|
58
|
+
elsif Rails.application.assets_manifest
|
|
59
|
+
Rails.application.assets_manifest.assets[logical_path].present?
|
|
60
|
+
else
|
|
61
|
+
false
|
|
62
|
+
end
|
|
63
|
+
rescue StandardError
|
|
64
|
+
false
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<% content_for :title, "JSAdmin Models" %>
|
|
2
|
+
|
|
3
|
+
<div class="mb-6">
|
|
4
|
+
<h1 class="text-2xl font-semibold text-slate-950">Models</h1>
|
|
5
|
+
<p class="mt-1 text-sm text-slate-600"><%= pluralize(@resources.size, "model") %> available</p>
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<div class="overflow-hidden rounded-lg border border-slate-200 bg-white shadow-sm">
|
|
9
|
+
<ul class="divide-y divide-slate-200">
|
|
10
|
+
<% @resources.each do |resource| %>
|
|
11
|
+
<li>
|
|
12
|
+
<%= link_to resources_path(resource.model_id), class: "flex items-center justify-between px-4 py-4 hover:bg-slate-50" do %>
|
|
13
|
+
<span>
|
|
14
|
+
<span class="block text-sm font-medium text-slate-950"><%= resource.label %></span>
|
|
15
|
+
<span class="mt-1 block text-xs text-slate-500"><%= resource.model_class.name %></span>
|
|
16
|
+
</span>
|
|
17
|
+
<span class="text-sm text-slate-500"><%= number_with_delimiter(resource.count) %></span>
|
|
18
|
+
<% end %>
|
|
19
|
+
</li>
|
|
20
|
+
<% end %>
|
|
21
|
+
</ul>
|
|
22
|
+
</div>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<%= form_with model: record, url: url, class: "space-y-6" do |form| %>
|
|
2
|
+
<% if record.errors.any? %>
|
|
3
|
+
<div class="rounded-md border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-800">
|
|
4
|
+
<p class="font-medium"><%= pluralize(record.errors.count, "error") %> prevented this record from being saved.</p>
|
|
5
|
+
<ul class="mt-2 list-disc space-y-1 pl-5">
|
|
6
|
+
<% record.errors.full_messages.each do |message| %>
|
|
7
|
+
<li><%= message %></li>
|
|
8
|
+
<% end %>
|
|
9
|
+
</ul>
|
|
10
|
+
</div>
|
|
11
|
+
<% end %>
|
|
12
|
+
|
|
13
|
+
<div class="rounded-lg border border-slate-200 bg-white shadow-sm">
|
|
14
|
+
<div class="divide-y divide-slate-200">
|
|
15
|
+
<% resource.editable_fields.each do |field| %>
|
|
16
|
+
<div class="grid gap-2 px-4 py-4 sm:grid-cols-3">
|
|
17
|
+
<%= form.label field.name, field.human_name, class: "text-sm font-medium text-slate-700" %>
|
|
18
|
+
<div class="sm:col-span-2">
|
|
19
|
+
<%= js_admin_field_input(form, field) %>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
<% end %>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div class="flex items-center gap-2">
|
|
27
|
+
<%= form.submit class: "inline-flex items-center justify-center rounded-md bg-sky-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-sky-700" %>
|
|
28
|
+
<%= link_to "Cancel", record.persisted? ? resource_path(resource.model_id, record) : resources_path(resource.model_id), class: "inline-flex items-center justify-center rounded-md border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50" %>
|
|
29
|
+
</div>
|
|
30
|
+
<% end %>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<% content_for :title, "Edit #{@resource.display_name(@record)}" %>
|
|
2
|
+
|
|
3
|
+
<div class="mb-6">
|
|
4
|
+
<div class="mb-2 text-sm text-slate-500">
|
|
5
|
+
<%= link_to "Models", root_path, class: "hover:text-slate-950" %> /
|
|
6
|
+
<%= link_to @resource.label, resources_path(@resource.model_id), class: "hover:text-slate-950" %> /
|
|
7
|
+
<%= link_to @resource.display_name(@record), resource_path(@resource.model_id, @record), class: "hover:text-slate-950" %> /
|
|
8
|
+
Edit
|
|
9
|
+
</div>
|
|
10
|
+
<h1 class="text-2xl font-semibold text-slate-950">Edit <%= @resource.display_name(@record) %></h1>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<%= render "form", record: @record, resource: @resource, url: resource_path(@resource.model_id, @record) %>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<% content_for :title, @resource.label %>
|
|
2
|
+
|
|
3
|
+
<div class="mb-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
|
4
|
+
<div>
|
|
5
|
+
<div class="mb-2 text-sm text-slate-500"><%= link_to "Models", root_path, class: "hover:text-slate-950" %> / <%= @resource.label %></div>
|
|
6
|
+
<h1 class="text-2xl font-semibold text-slate-950"><%= @resource.label %></h1>
|
|
7
|
+
<p class="mt-1 text-sm text-slate-600"><%= number_with_delimiter(@total_count) %> records</p>
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<%= link_to "New #{@resource.singular_label}", new_resource_path(@resource.model_id), class: "inline-flex items-center justify-center rounded-md bg-sky-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-sky-700" %>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<div class="overflow-x-auto rounded-lg border border-slate-200 bg-white shadow-sm">
|
|
14
|
+
<table class="min-w-full divide-y divide-slate-200">
|
|
15
|
+
<thead class="bg-slate-50">
|
|
16
|
+
<tr>
|
|
17
|
+
<% @resource.list_fields.each do |field| %>
|
|
18
|
+
<th scope="col" class="whitespace-nowrap px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500"><%= field.human_name %></th>
|
|
19
|
+
<% end %>
|
|
20
|
+
<th scope="col" class="w-24 px-4 py-3 text-right text-xs font-semibold uppercase tracking-wide text-slate-500">Actions</th>
|
|
21
|
+
</tr>
|
|
22
|
+
</thead>
|
|
23
|
+
<tbody class="divide-y divide-slate-200 bg-white">
|
|
24
|
+
<% @records.each do |record| %>
|
|
25
|
+
<tr class="hover:bg-slate-50">
|
|
26
|
+
<% @resource.list_fields.each do |field| %>
|
|
27
|
+
<td class="whitespace-nowrap px-4 py-3 text-sm"><%= js_admin_table_value(field, record) %></td>
|
|
28
|
+
<% end %>
|
|
29
|
+
<td class="whitespace-nowrap px-4 py-3 text-right text-sm">
|
|
30
|
+
<%= link_to "Open", resource_path(@resource.model_id, record), class: "font-medium text-sky-700 hover:text-sky-900" %>
|
|
31
|
+
</td>
|
|
32
|
+
</tr>
|
|
33
|
+
<% end %>
|
|
34
|
+
|
|
35
|
+
<% if @records.empty? %>
|
|
36
|
+
<tr>
|
|
37
|
+
<td colspan="<%= @resource.list_fields.size + 1 %>" class="px-4 py-10 text-center text-sm text-slate-500">No records</td>
|
|
38
|
+
</tr>
|
|
39
|
+
<% end %>
|
|
40
|
+
</tbody>
|
|
41
|
+
</table>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<div class="mt-4 flex items-center justify-between">
|
|
45
|
+
<div class="text-sm text-slate-500">Page <%= @page %></div>
|
|
46
|
+
<div class="flex gap-2">
|
|
47
|
+
<% if @resource.previous_page?(@page) %>
|
|
48
|
+
<%= link_to "Previous", resources_path(@resource.model_id, page: @page - 1), class: "rounded-md border border-slate-300 bg-white px-3 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50" %>
|
|
49
|
+
<% end %>
|
|
50
|
+
<% if @resource.next_page?(@page) %>
|
|
51
|
+
<%= link_to "Next", resources_path(@resource.model_id, page: @page + 1), class: "rounded-md border border-slate-300 bg-white px-3 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50" %>
|
|
52
|
+
<% end %>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<% content_for :title, "New #{@resource.singular_label}" %>
|
|
2
|
+
|
|
3
|
+
<div class="mb-6">
|
|
4
|
+
<div class="mb-2 text-sm text-slate-500">
|
|
5
|
+
<%= link_to "Models", root_path, class: "hover:text-slate-950" %> /
|
|
6
|
+
<%= link_to @resource.label, resources_path(@resource.model_id), class: "hover:text-slate-950" %> /
|
|
7
|
+
New
|
|
8
|
+
</div>
|
|
9
|
+
<h1 class="text-2xl font-semibold text-slate-950">New <%= @resource.singular_label %></h1>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<%= render "form", record: @record, resource: @resource, url: resources_path(@resource.model_id) %>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<% content_for :title, @resource.display_name(@record) %>
|
|
2
|
+
|
|
3
|
+
<div class="mb-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
|
4
|
+
<div>
|
|
5
|
+
<div class="mb-2 text-sm text-slate-500">
|
|
6
|
+
<%= link_to "Models", root_path, class: "hover:text-slate-950" %> /
|
|
7
|
+
<%= link_to @resource.label, resources_path(@resource.model_id), class: "hover:text-slate-950" %> /
|
|
8
|
+
<%= @resource.display_name(@record) %>
|
|
9
|
+
</div>
|
|
10
|
+
<h1 class="text-2xl font-semibold text-slate-950"><%= @resource.display_name(@record) %></h1>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<div class="flex gap-2">
|
|
14
|
+
<%= link_to "Edit", edit_resource_path(@resource.model_id, @record), class: "inline-flex items-center justify-center rounded-md border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50" %>
|
|
15
|
+
<%= button_to "Delete", resource_path(@resource.model_id, @record), method: :delete, form: { data: { turbo_confirm: "Delete this record?" } }, class: "inline-flex items-center justify-center rounded-md bg-rose-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-rose-700" %>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<div class="overflow-hidden rounded-lg border border-slate-200 bg-white shadow-sm">
|
|
20
|
+
<dl class="divide-y divide-slate-200">
|
|
21
|
+
<% @resource.fields.each do |field| %>
|
|
22
|
+
<div class="grid gap-2 px-4 py-4 sm:grid-cols-3">
|
|
23
|
+
<dt class="text-sm font-medium text-slate-600"><%= field.human_name %></dt>
|
|
24
|
+
<dd class="text-sm text-slate-950 sm:col-span-2"><%= js_admin_table_value(field, @record) %></dd>
|
|
25
|
+
</div>
|
|
26
|
+
<% end %>
|
|
27
|
+
</dl>
|
|
28
|
+
</div>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title><%= content_for(:title) || "JSAdmin" %></title>
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<%= csrf_meta_tags %>
|
|
7
|
+
<%= csp_meta_tag %>
|
|
8
|
+
|
|
9
|
+
<%= yield :head %>
|
|
10
|
+
|
|
11
|
+
<%= js_admin_stylesheet_link_tag "tailwind" %>
|
|
12
|
+
<%= stylesheet_link_tag "js_admin/application", media: "all", "data-turbo-track": "reload" %>
|
|
13
|
+
<%= js_admin_hotwire_javascript_tags %>
|
|
14
|
+
</head>
|
|
15
|
+
<body class="min-h-screen bg-slate-50 text-slate-950">
|
|
16
|
+
<div class="min-h-screen">
|
|
17
|
+
<header class="border-b border-slate-200 bg-white">
|
|
18
|
+
<div class="mx-auto flex max-w-7xl items-center justify-between px-4 py-4 sm:px-6 lg:px-8">
|
|
19
|
+
<%= link_to "JSAdmin", root_path, class: "text-base font-semibold text-slate-950" %>
|
|
20
|
+
<%= link_to "Models", root_path, class: "rounded-md px-3 py-2 text-sm font-medium text-slate-600 hover:bg-slate-100 hover:text-slate-950" %>
|
|
21
|
+
</div>
|
|
22
|
+
</header>
|
|
23
|
+
|
|
24
|
+
<main class="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
|
|
25
|
+
<% if notice.present? %>
|
|
26
|
+
<div class="mb-4 rounded-md border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-800"><%= notice %></div>
|
|
27
|
+
<% end %>
|
|
28
|
+
|
|
29
|
+
<% if alert.present? %>
|
|
30
|
+
<div class="mb-4 rounded-md border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-800"><%= alert %></div>
|
|
31
|
+
<% end %>
|
|
32
|
+
|
|
33
|
+
<%= yield %>
|
|
34
|
+
</main>
|
|
35
|
+
</div>
|
|
36
|
+
</body>
|
|
37
|
+
</html>
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
JSAdmin::Engine.routes.draw do
|
|
2
|
+
root to: "models#index"
|
|
3
|
+
|
|
4
|
+
get "/:model_id", to: "resources#index", as: :resources
|
|
5
|
+
get "/:model_id/new", to: "resources#new", as: :new_resource
|
|
6
|
+
post "/:model_id", to: "resources#create"
|
|
7
|
+
get "/:model_id/:id", to: "resources#show", as: :resource
|
|
8
|
+
get "/:model_id/:id/edit", to: "resources#edit", as: :edit_resource
|
|
9
|
+
patch "/:model_id/:id", to: "resources#update"
|
|
10
|
+
put "/:model_id/:id", to: "resources#update"
|
|
11
|
+
delete "/:model_id/:id", to: "resources#destroy"
|
|
12
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
require "rails/generators"
|
|
2
|
+
|
|
3
|
+
module JSAdmin
|
|
4
|
+
module Generators
|
|
5
|
+
class InstallGenerator < Rails::Generators::Base
|
|
6
|
+
class_option :path, type: :string, default: "/js_admin",
|
|
7
|
+
desc: "Mount path for JSAdmin"
|
|
8
|
+
|
|
9
|
+
def mount_engine
|
|
10
|
+
route %(mount JSAdmin::Engine => "#{options[:path]}")
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def import_tailwind_engine_styles
|
|
14
|
+
tailwind_entrypoint = Rails.root.join("app/assets/tailwind/application.css")
|
|
15
|
+
return unless tailwind_entrypoint.exist?
|
|
16
|
+
|
|
17
|
+
import_line = '@import "../builds/tailwind/js_admin";'
|
|
18
|
+
return if tailwind_entrypoint.read.include?(import_line)
|
|
19
|
+
|
|
20
|
+
append_to_file tailwind_entrypoint, "\n#{import_line}\n"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module JSAdmin
|
|
2
|
+
class Configuration
|
|
3
|
+
attr_accessor :included_models, :excluded_models, :records_per_page,
|
|
4
|
+
:association_limit, :display_name_methods
|
|
5
|
+
|
|
6
|
+
def initialize
|
|
7
|
+
@included_models = []
|
|
8
|
+
@excluded_models = []
|
|
9
|
+
@records_per_page = 25
|
|
10
|
+
@association_limit = 100
|
|
11
|
+
@display_name_methods = %i[name title email username id]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def include_models(*models)
|
|
15
|
+
included_models.concat(normalize_model_names(models))
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def exclude_models(*models)
|
|
19
|
+
excluded_models.concat(normalize_model_names(models))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def normalize_model_names(models)
|
|
25
|
+
models.flatten.map { |model| model.is_a?(Class) ? model.name : model.to_s }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module JSAdmin
|
|
2
|
+
class Engine < ::Rails::Engine
|
|
3
|
+
initializer "js_admin.inflections", before: :set_autoload_paths do
|
|
4
|
+
ActiveSupport::Inflector.inflections(:en) do |inflect|
|
|
5
|
+
inflect.acronym "JS"
|
|
6
|
+
end
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
isolate_namespace JSAdmin
|
|
10
|
+
|
|
11
|
+
config.to_prepare do
|
|
12
|
+
JSAdmin::Registry.reset!
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
module JSAdmin
|
|
2
|
+
class Field
|
|
3
|
+
attr_reader :resource, :column
|
|
4
|
+
|
|
5
|
+
def initialize(resource, column)
|
|
6
|
+
@resource = resource
|
|
7
|
+
@column = column
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def name
|
|
11
|
+
column.name
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def human_name
|
|
15
|
+
resource.model_class.human_attribute_name(name)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def type
|
|
19
|
+
column.type
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def primary_key?
|
|
23
|
+
name == resource.model_class.primary_key
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def timestamp?
|
|
27
|
+
%w[created_at updated_at].include?(name)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def editable?
|
|
31
|
+
!primary_key? &&
|
|
32
|
+
!timestamp? &&
|
|
33
|
+
!inheritance_column? &&
|
|
34
|
+
!readonly_attribute? &&
|
|
35
|
+
!generated_column?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def input_kind
|
|
39
|
+
return :association_select if belongs_to_association
|
|
40
|
+
return :enum_select if enum?
|
|
41
|
+
|
|
42
|
+
case type
|
|
43
|
+
when :text
|
|
44
|
+
:text_area
|
|
45
|
+
when :boolean
|
|
46
|
+
:check_box
|
|
47
|
+
when :integer, :bigint, :float, :decimal
|
|
48
|
+
:number_field
|
|
49
|
+
when :date
|
|
50
|
+
:date_field
|
|
51
|
+
when :datetime
|
|
52
|
+
:datetime_field
|
|
53
|
+
when :time
|
|
54
|
+
:time_field
|
|
55
|
+
else
|
|
56
|
+
:text_field
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def enum?
|
|
61
|
+
enum_values.any?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def enum_values
|
|
65
|
+
resource.model_class.defined_enums.fetch(name, {}).keys
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def belongs_to_association
|
|
69
|
+
@belongs_to_association ||= resource.model_class
|
|
70
|
+
.reflect_on_all_associations(:belongs_to)
|
|
71
|
+
.find { |association| association.foreign_key.to_s == name }
|
|
72
|
+
rescue NameError
|
|
73
|
+
nil
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def association_options
|
|
77
|
+
association = belongs_to_association
|
|
78
|
+
return [] unless association
|
|
79
|
+
|
|
80
|
+
association_resource = Resource.new(association.klass)
|
|
81
|
+
association.klass
|
|
82
|
+
.limit(JSAdmin.configuration.association_limit)
|
|
83
|
+
.map { |record| [ association_resource.display_name(record), record.id ] }
|
|
84
|
+
rescue ActiveRecord::ActiveRecordError, NameError
|
|
85
|
+
[]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def number_step
|
|
89
|
+
%i[float decimal].include?(type) ? "any" : 1
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def value_for(record)
|
|
93
|
+
record.public_send(name)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def formatted_value(record)
|
|
97
|
+
value = value_for(record)
|
|
98
|
+
|
|
99
|
+
case value
|
|
100
|
+
when nil
|
|
101
|
+
"NULL"
|
|
102
|
+
when true
|
|
103
|
+
"true"
|
|
104
|
+
when false
|
|
105
|
+
"false"
|
|
106
|
+
else
|
|
107
|
+
value.to_s
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
def inheritance_column?
|
|
114
|
+
name == resource.model_class.inheritance_column
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def readonly_attribute?
|
|
118
|
+
resource.model_class.readonly_attributes.include?(name)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def generated_column?
|
|
122
|
+
column.respond_to?(:generated?) && column.generated?
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
module JSAdmin
|
|
2
|
+
class Registry
|
|
3
|
+
class << self
|
|
4
|
+
def resources
|
|
5
|
+
@resources ||= build_resources
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def resource_for!(model_id)
|
|
9
|
+
resources.find { |resource| resource.model_id == model_id } ||
|
|
10
|
+
raise(ActiveRecord::RecordNotFound, "Unknown JSAdmin resource: #{model_id}")
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def reset!
|
|
14
|
+
@resources = nil
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def build_resources
|
|
20
|
+
eager_load_application_models
|
|
21
|
+
|
|
22
|
+
ActiveRecord::Base.descendants
|
|
23
|
+
.select { |model_class| manageable_model?(model_class) }
|
|
24
|
+
.select { |model_class| included_model?(model_class) }
|
|
25
|
+
.reject { |model_class| excluded_model?(model_class) }
|
|
26
|
+
.map { |model_class| Resource.new(model_class) }
|
|
27
|
+
.sort_by(&:label)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def eager_load_application_models
|
|
31
|
+
Rails.application.eager_load! unless Rails.application.config.eager_load
|
|
32
|
+
rescue Zeitwerk::NameError
|
|
33
|
+
raise
|
|
34
|
+
rescue StandardError
|
|
35
|
+
nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def manageable_model?(model_class)
|
|
39
|
+
model_class.name.present? &&
|
|
40
|
+
!model_class.name.start_with?("ActiveRecord::") &&
|
|
41
|
+
!model_class.abstract_class? &&
|
|
42
|
+
model_class < ActiveRecord::Base &&
|
|
43
|
+
model_class.table_exists?
|
|
44
|
+
rescue ActiveRecord::ActiveRecordError, StandardError
|
|
45
|
+
false
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def included_model?(model_class)
|
|
49
|
+
included_models = JSAdmin.configuration.included_models
|
|
50
|
+
included_models.empty? || included_models.include?(model_class.name)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def excluded_model?(model_class)
|
|
54
|
+
JSAdmin.configuration.excluded_models.include?(model_class.name)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
module JSAdmin
|
|
2
|
+
class Resource
|
|
3
|
+
attr_reader :model_class
|
|
4
|
+
|
|
5
|
+
def initialize(model_class)
|
|
6
|
+
@model_class = model_class
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def model_id
|
|
10
|
+
model_class.name.underscore.tr("/", "--")
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def label
|
|
14
|
+
model_class.model_name.human(count: 2)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def singular_label
|
|
18
|
+
model_class.model_name.human
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def param_key
|
|
22
|
+
model_class.model_name.param_key
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def fields
|
|
26
|
+
@fields ||= model_class.columns.map { |column| Field.new(self, column) }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def editable_fields
|
|
30
|
+
fields.select(&:editable?)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def list_fields
|
|
34
|
+
visible_fields = fields.reject(&:timestamp?)
|
|
35
|
+
id_field = visible_fields.find(&:primary_key?)
|
|
36
|
+
selected_fields = visible_fields.reject(&:primary_key?).first(6)
|
|
37
|
+
|
|
38
|
+
[ id_field, *selected_fields ].compact
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def records(page:)
|
|
42
|
+
page_number = [ page.to_i, 1 ].max
|
|
43
|
+
model_class.all
|
|
44
|
+
.then { |scope| ordered_scope(scope) }
|
|
45
|
+
.limit(JSAdmin.configuration.records_per_page)
|
|
46
|
+
.offset((page_number - 1) * JSAdmin.configuration.records_per_page)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def count
|
|
50
|
+
model_class.count
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def find(record_id)
|
|
54
|
+
model_class.find(record_id)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def new_record(attributes = {})
|
|
58
|
+
model_class.new(attributes)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def display_name(record)
|
|
62
|
+
display_method = JSAdmin.configuration.display_name_methods.find do |method_name|
|
|
63
|
+
record.respond_to?(method_name) && record.public_send(method_name).present?
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
return record.public_send(display_method).to_s if display_method
|
|
67
|
+
|
|
68
|
+
"#{singular_label} ##{record.id}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def next_page?(page)
|
|
72
|
+
count > page.to_i * JSAdmin.configuration.records_per_page
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def previous_page?(page)
|
|
76
|
+
page.to_i > 1
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def ordered_scope(scope)
|
|
82
|
+
primary_key = model_class.primary_key
|
|
83
|
+
return scope unless primary_key && model_class.column_names.include?(primary_key)
|
|
84
|
+
|
|
85
|
+
scope.order(primary_key => :desc)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
data/lib/js_admin.rb
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
require "js_admin/version"
|
|
2
|
+
require "js_admin/configuration"
|
|
3
|
+
require "js_admin/field"
|
|
4
|
+
require "js_admin/registry"
|
|
5
|
+
require "js_admin/resource"
|
|
6
|
+
require "js_admin/engine"
|
|
7
|
+
|
|
8
|
+
module JSAdmin
|
|
9
|
+
class << self
|
|
10
|
+
def configuration
|
|
11
|
+
@configuration ||= Configuration.new
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def configure
|
|
15
|
+
yield configuration
|
|
16
|
+
Registry.reset!
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def resources
|
|
20
|
+
Registry.resources
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def resource_for!(model_id)
|
|
24
|
+
Registry.resource_for!(model_id)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: js_admin
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- JoungSik
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 2026-07-12 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rails
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 8.1.3
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '9.0'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: 8.1.3
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '9.0'
|
|
32
|
+
- !ruby/object:Gem::Dependency
|
|
33
|
+
name: stimulus-rails
|
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - ">="
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: 1.3.4
|
|
39
|
+
- - "<"
|
|
40
|
+
- !ruby/object:Gem::Version
|
|
41
|
+
version: '2.0'
|
|
42
|
+
type: :runtime
|
|
43
|
+
prerelease: false
|
|
44
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - ">="
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: 1.3.4
|
|
49
|
+
- - "<"
|
|
50
|
+
- !ruby/object:Gem::Version
|
|
51
|
+
version: '2.0'
|
|
52
|
+
- !ruby/object:Gem::Dependency
|
|
53
|
+
name: tailwindcss-rails
|
|
54
|
+
requirement: !ruby/object:Gem::Requirement
|
|
55
|
+
requirements:
|
|
56
|
+
- - ">="
|
|
57
|
+
- !ruby/object:Gem::Version
|
|
58
|
+
version: 4.4.0
|
|
59
|
+
- - "<"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '5.0'
|
|
62
|
+
type: :runtime
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: 4.4.0
|
|
69
|
+
- - "<"
|
|
70
|
+
- !ruby/object:Gem::Version
|
|
71
|
+
version: '5.0'
|
|
72
|
+
- !ruby/object:Gem::Dependency
|
|
73
|
+
name: turbo-rails
|
|
74
|
+
requirement: !ruby/object:Gem::Requirement
|
|
75
|
+
requirements:
|
|
76
|
+
- - ">="
|
|
77
|
+
- !ruby/object:Gem::Version
|
|
78
|
+
version: 2.0.23
|
|
79
|
+
- - "<"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '3.0'
|
|
82
|
+
type: :runtime
|
|
83
|
+
prerelease: false
|
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - ">="
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: 2.0.23
|
|
89
|
+
- - "<"
|
|
90
|
+
- !ruby/object:Gem::Version
|
|
91
|
+
version: '3.0'
|
|
92
|
+
description: JSAdmin exposes Active Record models through a generic CRUD UI built
|
|
93
|
+
with Tailwind CSS and Hotwire.
|
|
94
|
+
email:
|
|
95
|
+
- tjstlr2010@gmail.com
|
|
96
|
+
executables: []
|
|
97
|
+
extensions: []
|
|
98
|
+
extra_rdoc_files: []
|
|
99
|
+
files:
|
|
100
|
+
- CHANGELOG.md
|
|
101
|
+
- MIT-LICENSE
|
|
102
|
+
- README.md
|
|
103
|
+
- Rakefile
|
|
104
|
+
- app/assets/stylesheets/js_admin/application.css
|
|
105
|
+
- app/assets/tailwind/js_admin/engine.css
|
|
106
|
+
- app/controllers/js_admin/application_controller.rb
|
|
107
|
+
- app/controllers/js_admin/models_controller.rb
|
|
108
|
+
- app/controllers/js_admin/resources_controller.rb
|
|
109
|
+
- app/helpers/js_admin/application_helper.rb
|
|
110
|
+
- app/models/js_admin/application_record.rb
|
|
111
|
+
- app/views/js_admin/models/index.html.erb
|
|
112
|
+
- app/views/js_admin/resources/_form.html.erb
|
|
113
|
+
- app/views/js_admin/resources/edit.html.erb
|
|
114
|
+
- app/views/js_admin/resources/index.html.erb
|
|
115
|
+
- app/views/js_admin/resources/new.html.erb
|
|
116
|
+
- app/views/js_admin/resources/show.html.erb
|
|
117
|
+
- app/views/layouts/js_admin/application.html.erb
|
|
118
|
+
- config/routes.rb
|
|
119
|
+
- lib/generators/js_admin/install/install_generator.rb
|
|
120
|
+
- lib/js_admin.rb
|
|
121
|
+
- lib/js_admin/configuration.rb
|
|
122
|
+
- lib/js_admin/engine.rb
|
|
123
|
+
- lib/js_admin/field.rb
|
|
124
|
+
- lib/js_admin/registry.rb
|
|
125
|
+
- lib/js_admin/resource.rb
|
|
126
|
+
- lib/js_admin/version.rb
|
|
127
|
+
- lib/tasks/js_admin_tasks.rake
|
|
128
|
+
homepage: https://github.com/JoungSik/js_admin
|
|
129
|
+
licenses:
|
|
130
|
+
- MIT
|
|
131
|
+
metadata:
|
|
132
|
+
homepage_uri: https://github.com/JoungSik/js_admin
|
|
133
|
+
source_code_uri: https://github.com/JoungSik/js_admin/tree/main
|
|
134
|
+
changelog_uri: https://github.com/JoungSik/js_admin/CHANGELOG.md
|
|
135
|
+
rdoc_options: []
|
|
136
|
+
require_paths:
|
|
137
|
+
- lib
|
|
138
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
139
|
+
requirements:
|
|
140
|
+
- - ">="
|
|
141
|
+
- !ruby/object:Gem::Version
|
|
142
|
+
version: 3.2.0
|
|
143
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
144
|
+
requirements:
|
|
145
|
+
- - ">="
|
|
146
|
+
- !ruby/object:Gem::Version
|
|
147
|
+
version: '0'
|
|
148
|
+
requirements: []
|
|
149
|
+
rubygems_version: 3.6.2
|
|
150
|
+
specification_version: 4
|
|
151
|
+
summary: Zero-configuration CRUD engine for Rails applications.
|
|
152
|
+
test_files: []
|