js_admin 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +24 -0
- data/README.md +227 -19
- data/app/assets/javascripts/js_admin/application.js +13 -0
- data/app/assets/javascripts/js_admin/controllers/flash_controller.js +23 -0
- data/app/assets/javascripts/js_admin/controllers/theme_controller.js +62 -0
- data/app/assets/tailwind/js_admin/engine.css +4 -0
- data/app/controllers/js_admin/application_controller.rb +45 -1
- data/app/controllers/js_admin/home_controller.rb +6 -0
- data/app/controllers/js_admin/resources_controller.rb +21 -6
- data/app/helpers/js_admin/application_helper.rb +292 -19
- data/app/views/js_admin/home/index.html.erb +5 -0
- data/app/views/js_admin/models/_index_content.html.erb +20 -0
- data/app/views/js_admin/models/index.html.erb +4 -21
- data/app/views/js_admin/resources/_edit_content.html.erb +11 -0
- data/app/views/js_admin/resources/_form.html.erb +21 -13
- data/app/views/js_admin/resources/_index_content.html.erb +74 -0
- data/app/views/js_admin/resources/_new_content.html.erb +10 -0
- data/app/views/js_admin/resources/_show_content.html.erb +26 -0
- data/app/views/js_admin/resources/destroy.turbo_stream.erb +7 -0
- data/app/views/js_admin/resources/edit.html.erb +4 -12
- data/app/views/js_admin/resources/index.html.erb +3 -52
- data/app/views/js_admin/resources/new.html.erb +4 -11
- data/app/views/js_admin/resources/show.html.erb +3 -26
- data/app/views/layouts/js_admin/_content_frame.html.erb +7 -0
- data/app/views/layouts/js_admin/_flash.html.erb +3 -0
- data/app/views/layouts/js_admin/_flash_messages.html.erb +9 -0
- data/app/views/layouts/js_admin/_sidebar.html.erb +36 -0
- data/app/views/layouts/js_admin/application.html.erb +19 -17
- data/config/locales/js_admin.en.yml +65 -0
- data/config/locales/js_admin.ko.yml +65 -0
- data/config/routes.rb +2 -1
- data/lib/generators/js_admin/install/install_generator.rb +70 -5
- data/lib/js_admin/configuration.rb +82 -3
- data/lib/js_admin/configuration_error.rb +4 -0
- data/lib/js_admin/field.rb +21 -7
- data/lib/js_admin/registry.rb +4 -1
- data/lib/js_admin/resource.rb +17 -5
- data/lib/js_admin/version.rb +1 -1
- data/lib/js_admin.rb +8 -0
- metadata +40 -2
|
@@ -1,54 +1,5 @@
|
|
|
1
1
|
<% content_for :title, @resource.label %>
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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>
|
|
3
|
+
<%= render layout: "layouts/js_admin/content_frame" do %>
|
|
4
|
+
<%= render "index_content" %>
|
|
5
|
+
<% end %>
|
|
@@ -1,12 +1,5 @@
|
|
|
1
|
-
<% content_for :title, "
|
|
1
|
+
<% content_for :title, t("js_admin.titles.new", resource: @resource.singular_label) %>
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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) %>
|
|
3
|
+
<%= render layout: "layouts/js_admin/content_frame" do %>
|
|
4
|
+
<%= render "new_content" %>
|
|
5
|
+
<% end %>
|
|
@@ -1,28 +1,5 @@
|
|
|
1
1
|
<% content_for :title, @resource.display_name(@record) %>
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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>
|
|
3
|
+
<%= render layout: "layouts/js_admin/content_frame" do %>
|
|
4
|
+
<%= render "show_content" %>
|
|
5
|
+
<% end %>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<div class="w-full max-w-sm space-y-3">
|
|
2
|
+
<% if notice.present? %>
|
|
3
|
+
<div data-controller="js-admin-flash" data-js-admin-flash-timeout-value="4000" class="<%= js_admin_flash_classes(:notice) %>" role="status"><%= notice %></div>
|
|
4
|
+
<% end %>
|
|
5
|
+
|
|
6
|
+
<% if alert.present? %>
|
|
7
|
+
<div data-controller="js-admin-flash" data-js-admin-flash-timeout-value="4000" class="<%= js_admin_flash_classes(:alert) %>" role="alert"><%= alert %></div>
|
|
8
|
+
<% end %>
|
|
9
|
+
</div>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<aside class="flex shrink-0 flex-col border-b border-slate-200 bg-white/80 dark:border-slate-800 dark:bg-slate-900/70 lg:sticky lg:top-16 lg:h-[calc(100vh-4rem)] lg:w-64 lg:self-start lg:overflow-hidden lg:border-b-0 lg:border-r">
|
|
2
|
+
<nav class="min-h-0 flex-1 space-y-6 overflow-y-auto px-5 py-6 sm:px-8 lg:px-5">
|
|
3
|
+
<section>
|
|
4
|
+
<div class="space-y-1">
|
|
5
|
+
<% js_admin_sidebar_top_level_resources.each do |resource| %>
|
|
6
|
+
<%= link_to js_admin_sidebar_resource_label(resource), resources_path(resource.model_id), class: js_admin_sidebar_link_classes(resource), data: js_admin_content_link_data.merge(js_admin_sidebar_link: resource.model_id) %>
|
|
7
|
+
<% end %>
|
|
8
|
+
|
|
9
|
+
<% js_admin_sidebar_namespace_groups.each do |namespace, resources| %>
|
|
10
|
+
<details class="group" <%= "open" if js_admin_sidebar_namespace_open?(resources) %>>
|
|
11
|
+
<summary class="flex cursor-pointer items-center justify-between rounded-md px-3 py-2 text-xs font-semibold uppercase tracking-wide text-slate-500 hover:bg-slate-100 hover:text-slate-950 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-100">
|
|
12
|
+
<span><%= js_admin_sidebar_namespace_label(namespace) %></span>
|
|
13
|
+
<span class="transition-transform group-open:rotate-90">></span>
|
|
14
|
+
</summary>
|
|
15
|
+
|
|
16
|
+
<div class="mt-1 space-y-1 pl-3">
|
|
17
|
+
<% resources.each do |resource| %>
|
|
18
|
+
<%= link_to js_admin_sidebar_resource_label(resource), resources_path(resource.model_id), class: js_admin_sidebar_link_classes(resource), data: js_admin_content_link_data.merge(js_admin_sidebar_link: resource.model_id) %>
|
|
19
|
+
<% end %>
|
|
20
|
+
</div>
|
|
21
|
+
</details>
|
|
22
|
+
<% end %>
|
|
23
|
+
</div>
|
|
24
|
+
</section>
|
|
25
|
+
</nav>
|
|
26
|
+
|
|
27
|
+
<div class="shrink-0 border-t border-slate-200 px-5 py-4 dark:border-slate-800 sm:px-8 lg:px-5">
|
|
28
|
+
<button type="button" data-js-admin-theme-target="toggle" data-action="js-admin-theme#toggle" class="inline-flex h-9 w-full items-center justify-between rounded-md border border-slate-300 bg-white px-3 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200 dark:hover:bg-slate-800" aria-label="<%= t("js_admin.theme.toggle_aria_label") %>" aria-pressed="false">
|
|
29
|
+
<span class="flex items-center gap-2">
|
|
30
|
+
<span class="h-2.5 w-2.5 rounded-full bg-slate-500 dark:bg-amber-300"></span>
|
|
31
|
+
<span><%= t("js_admin.theme.label") %></span>
|
|
32
|
+
</span>
|
|
33
|
+
<span class="text-xs text-slate-400 dark:text-slate-500"><%= t("js_admin.theme.toggle") %></span>
|
|
34
|
+
</button>
|
|
35
|
+
</div>
|
|
36
|
+
</aside>
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
|
-
<html>
|
|
2
|
+
<html class="<%= js_admin_html_classes %>">
|
|
3
3
|
<head>
|
|
4
4
|
<title><%= content_for(:title) || "JSAdmin" %></title>
|
|
5
5
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<meta name="color-scheme" content="light dark">
|
|
6
7
|
<%= csrf_meta_tags %>
|
|
7
8
|
<%= csp_meta_tag %>
|
|
8
9
|
|
|
@@ -10,28 +11,29 @@
|
|
|
10
11
|
|
|
11
12
|
<%= js_admin_stylesheet_link_tag "tailwind" %>
|
|
12
13
|
<%= stylesheet_link_tag "js_admin/application", media: "all", "data-turbo-track": "reload" %>
|
|
13
|
-
<%=
|
|
14
|
+
<%= js_admin_javascript_tags %>
|
|
14
15
|
</head>
|
|
15
|
-
<body class="min-h-screen bg-slate-50 text-slate-950">
|
|
16
|
+
<body class="min-h-screen bg-slate-50 text-slate-950 antialiased dark:bg-slate-950 dark:text-slate-100" data-controller="js-admin-theme">
|
|
16
17
|
<div class="min-h-screen">
|
|
17
|
-
<header class="border-b border-slate-200 bg-white">
|
|
18
|
-
<div class="
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
<header class="fixed inset-x-0 top-0 z-40 border-b border-slate-200 bg-white/95 dark:border-slate-800 dark:bg-slate-900/95">
|
|
19
|
+
<div class="flex min-h-16 w-full items-center">
|
|
20
|
+
<div class="flex min-h-16 shrink-0 items-center px-5 sm:px-8 lg:w-64 lg:border-r lg:border-slate-200 lg:px-5 lg:dark:border-slate-800">
|
|
21
|
+
<%= link_to "JSAdmin", root_path, class: "text-base font-semibold text-slate-950 dark:text-slate-100", data: js_admin_content_link_data %>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<div class="min-h-16 min-w-0 flex-1"></div>
|
|
21
25
|
</div>
|
|
22
26
|
</header>
|
|
23
27
|
|
|
24
|
-
<
|
|
25
|
-
|
|
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 %>
|
|
28
|
+
<div class="flex w-full flex-col pt-16 lg:flex-row">
|
|
29
|
+
<%= render "layouts/js_admin/sidebar" %>
|
|
32
30
|
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
<main class="min-w-0 flex-1 px-5 py-8 sm:px-8 sm:py-10 lg:px-10">
|
|
32
|
+
<div class="mx-auto max-w-screen-2xl">
|
|
33
|
+
<%= yield %>
|
|
34
|
+
</div>
|
|
35
|
+
</main>
|
|
36
|
+
</div>
|
|
35
37
|
</div>
|
|
36
38
|
</body>
|
|
37
39
|
</html>
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
en:
|
|
2
|
+
js_admin:
|
|
3
|
+
configuration:
|
|
4
|
+
errors:
|
|
5
|
+
incomplete_basic_auth: "Basic Auth username and password must both be configured."
|
|
6
|
+
multiple_authentication_strategies: "Basic Auth and authenticate_with cannot be configured at the same time."
|
|
7
|
+
authorization_without_authentication: "authorize_with requires authenticate_with."
|
|
8
|
+
generator:
|
|
9
|
+
options:
|
|
10
|
+
path: "Mount path for JSAdmin"
|
|
11
|
+
warnings:
|
|
12
|
+
importmap_missing: "config/importmap.rb was not found. Run bin/rails importmap:install, then rerun js_admin:install."
|
|
13
|
+
titles:
|
|
14
|
+
models: "JSAdmin Models"
|
|
15
|
+
new: "New %{resource}"
|
|
16
|
+
edit: "Edit %{record}"
|
|
17
|
+
navigation:
|
|
18
|
+
home: "Home"
|
|
19
|
+
models:
|
|
20
|
+
heading: "Models"
|
|
21
|
+
available:
|
|
22
|
+
one: "%{formatted_count} model available"
|
|
23
|
+
other: "%{formatted_count} models available"
|
|
24
|
+
resources:
|
|
25
|
+
notices:
|
|
26
|
+
created: "%{resource} was created."
|
|
27
|
+
updated: "%{resource} was updated."
|
|
28
|
+
deleted: "%{resource} was deleted."
|
|
29
|
+
records:
|
|
30
|
+
one: "%{formatted_count} record"
|
|
31
|
+
other: "%{formatted_count} records"
|
|
32
|
+
headings:
|
|
33
|
+
new: "New %{resource}"
|
|
34
|
+
edit: "Edit %{record}"
|
|
35
|
+
actions:
|
|
36
|
+
create: "Create %{resource}"
|
|
37
|
+
update: "Update %{resource}"
|
|
38
|
+
new: "New %{resource}"
|
|
39
|
+
view: "View"
|
|
40
|
+
edit: "Edit"
|
|
41
|
+
delete: "Delete"
|
|
42
|
+
cancel: "Cancel"
|
|
43
|
+
table:
|
|
44
|
+
actions: "Actions"
|
|
45
|
+
no_records: "No records"
|
|
46
|
+
delete_confirmation: "Delete this record?"
|
|
47
|
+
pagination:
|
|
48
|
+
label: "Pagination"
|
|
49
|
+
first: "First"
|
|
50
|
+
previous: "Previous"
|
|
51
|
+
next: "Next"
|
|
52
|
+
last: "Last"
|
|
53
|
+
status: "Page %{page} of %{total_pages}"
|
|
54
|
+
theme:
|
|
55
|
+
label: "Theme"
|
|
56
|
+
toggle: "Toggle"
|
|
57
|
+
toggle_aria_label: "Toggle dark mode"
|
|
58
|
+
values:
|
|
59
|
+
null: "NULL"
|
|
60
|
+
"true": "true"
|
|
61
|
+
"false": "false"
|
|
62
|
+
resource:
|
|
63
|
+
fallback_display_name: "%{resource} #%{id}"
|
|
64
|
+
errors:
|
|
65
|
+
unknown_resource: "Unknown JSAdmin resource: %{model_id}"
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
ko:
|
|
2
|
+
js_admin:
|
|
3
|
+
configuration:
|
|
4
|
+
errors:
|
|
5
|
+
incomplete_basic_auth: "Basic Auth username과 password를 모두 설정해야 합니다."
|
|
6
|
+
multiple_authentication_strategies: "Basic Auth와 authenticate_with는 동시에 설정할 수 없습니다."
|
|
7
|
+
authorization_without_authentication: "authorize_with를 사용하려면 authenticate_with가 필요합니다."
|
|
8
|
+
generator:
|
|
9
|
+
options:
|
|
10
|
+
path: "JSAdmin mount 경로"
|
|
11
|
+
warnings:
|
|
12
|
+
importmap_missing: "config/importmap.rb를 찾을 수 없습니다. bin/rails importmap:install 실행 후 js_admin:install을 다시 실행하세요."
|
|
13
|
+
titles:
|
|
14
|
+
models: "JSAdmin 모델"
|
|
15
|
+
new: "새 %{resource}"
|
|
16
|
+
edit: "%{record} 편집"
|
|
17
|
+
navigation:
|
|
18
|
+
home: "홈"
|
|
19
|
+
models:
|
|
20
|
+
heading: "모델"
|
|
21
|
+
available:
|
|
22
|
+
one: "사용 가능한 모델 %{formatted_count}개"
|
|
23
|
+
other: "사용 가능한 모델 %{formatted_count}개"
|
|
24
|
+
resources:
|
|
25
|
+
notices:
|
|
26
|
+
created: "%{resource} 레코드를 생성했습니다."
|
|
27
|
+
updated: "%{resource} 레코드를 수정했습니다."
|
|
28
|
+
deleted: "%{resource} 레코드를 삭제했습니다."
|
|
29
|
+
records:
|
|
30
|
+
one: "레코드 %{formatted_count}개"
|
|
31
|
+
other: "레코드 %{formatted_count}개"
|
|
32
|
+
headings:
|
|
33
|
+
new: "새 %{resource}"
|
|
34
|
+
edit: "%{record} 편집"
|
|
35
|
+
actions:
|
|
36
|
+
create: "%{resource} 생성"
|
|
37
|
+
update: "%{resource} 저장"
|
|
38
|
+
new: "새 %{resource}"
|
|
39
|
+
view: "보기"
|
|
40
|
+
edit: "편집"
|
|
41
|
+
delete: "삭제"
|
|
42
|
+
cancel: "취소"
|
|
43
|
+
table:
|
|
44
|
+
actions: "작업"
|
|
45
|
+
no_records: "레코드가 없습니다"
|
|
46
|
+
delete_confirmation: "이 레코드를 삭제하시겠습니까?"
|
|
47
|
+
pagination:
|
|
48
|
+
label: "페이지 탐색"
|
|
49
|
+
first: "처음"
|
|
50
|
+
previous: "이전"
|
|
51
|
+
next: "다음"
|
|
52
|
+
last: "마지막"
|
|
53
|
+
status: "전체 %{total_pages}페이지 중 %{page}페이지"
|
|
54
|
+
theme:
|
|
55
|
+
label: "테마"
|
|
56
|
+
toggle: "전환"
|
|
57
|
+
toggle_aria_label: "다크 모드 전환"
|
|
58
|
+
values:
|
|
59
|
+
null: "NULL"
|
|
60
|
+
"true": "참"
|
|
61
|
+
"false": "거짓"
|
|
62
|
+
resource:
|
|
63
|
+
fallback_display_name: "%{resource} #%{id}"
|
|
64
|
+
errors:
|
|
65
|
+
unknown_resource: "알 수 없는 JSAdmin 리소스입니다: %{model_id}"
|
data/config/routes.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
JSAdmin::Engine.routes.draw do
|
|
2
|
-
root to: "
|
|
2
|
+
root to: "home#index"
|
|
3
3
|
|
|
4
|
+
get "/models", to: "models#index", as: :models
|
|
4
5
|
get "/:model_id", to: "resources#index", as: :resources
|
|
5
6
|
get "/:model_id/new", to: "resources#new", as: :new_resource
|
|
6
7
|
post "/:model_id", to: "resources#create"
|
|
@@ -3,22 +3,87 @@ require "rails/generators"
|
|
|
3
3
|
module JSAdmin
|
|
4
4
|
module Generators
|
|
5
5
|
class InstallGenerator < Rails::Generators::Base
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
IMPORTMAP_PINS = {
|
|
7
|
+
"@hotwired/turbo-rails" => 'pin "@hotwired/turbo-rails", to: "turbo.min.js"',
|
|
8
|
+
"@hotwired/stimulus" => 'pin "@hotwired/stimulus", to: "stimulus.min.js"',
|
|
9
|
+
"js_admin" => 'pin "js_admin", to: "js_admin/application.js", preload: true',
|
|
10
|
+
"js_admin/controllers/theme_controller" => 'pin "js_admin/controllers/theme_controller", to: "js_admin/controllers/theme_controller.js", preload: true',
|
|
11
|
+
"js_admin/controllers/flash_controller" => 'pin "js_admin/controllers/flash_controller", to: "js_admin/controllers/flash_controller.js", preload: true'
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
14
|
+
namespace "js_admin:install"
|
|
15
|
+
|
|
16
|
+
class_option :path,
|
|
17
|
+
type: :string,
|
|
18
|
+
default: "/admin",
|
|
19
|
+
desc: I18n.t("js_admin.generator.options.path")
|
|
8
20
|
|
|
9
21
|
def mount_engine
|
|
10
22
|
route %(mount JSAdmin::Engine => "#{options[:path]}")
|
|
11
23
|
end
|
|
12
24
|
|
|
25
|
+
def create_initializer
|
|
26
|
+
create_file "config/initializers/js_admin.rb", <<~RUBY
|
|
27
|
+
JSAdmin.configure do |config|
|
|
28
|
+
# Limit visible models. When empty, JSAdmin exposes every concrete Active Record model.
|
|
29
|
+
# config.include_models User, Post
|
|
30
|
+
# config.exclude_models AuditLog
|
|
31
|
+
|
|
32
|
+
# UI pagination and association select limits.
|
|
33
|
+
# config.records_per_page = 10
|
|
34
|
+
# config.association_limit = 100
|
|
35
|
+
|
|
36
|
+
# Methods tried in order when displaying records in labels and associations.
|
|
37
|
+
# config.display_name_methods = %i[name title email username id]
|
|
38
|
+
|
|
39
|
+
# Render a column as a select. Callables are evaluated on every form render.
|
|
40
|
+
# config.select_options "Post", category: -> { Category.pluck(:name) }
|
|
41
|
+
|
|
42
|
+
# Basic Auth is disabled unless both username and password are present.
|
|
43
|
+
# config.basic_auth_username = ENV["JS_ADMIN_BASIC_AUTH_USERNAME"]
|
|
44
|
+
# config.basic_auth_password = ENV["JS_ADMIN_BASIC_AUTH_PASSWORD"]
|
|
45
|
+
# config.basic_auth_realm = "JSAdmin"
|
|
46
|
+
|
|
47
|
+
# Or delegate authentication and authorization to the host application.
|
|
48
|
+
# config.authenticate_with { authenticate_user! }
|
|
49
|
+
# config.authorize_with { current_user&.admin? }
|
|
50
|
+
end
|
|
51
|
+
RUBY
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def pin_importmap_modules
|
|
55
|
+
importmap_path = application_path("config/importmap.rb")
|
|
56
|
+
|
|
57
|
+
unless File.exist?(importmap_path)
|
|
58
|
+
say_status :warning, I18n.t("js_admin.generator.warnings.importmap_missing"), :yellow
|
|
59
|
+
return
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
importmap = File.read(importmap_path)
|
|
63
|
+
missing_pins = IMPORTMAP_PINS.filter_map do |name, line|
|
|
64
|
+
line unless importmap.match?(/^\s*pin\s+["']#{Regexp.escape(name)}["']/)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
return if missing_pins.empty?
|
|
68
|
+
|
|
69
|
+
append_to_file importmap_path, "\n#{missing_pins.join("\n")}\n"
|
|
70
|
+
end
|
|
71
|
+
|
|
13
72
|
def import_tailwind_engine_styles
|
|
14
|
-
tailwind_entrypoint =
|
|
15
|
-
return unless
|
|
73
|
+
tailwind_entrypoint = application_path("app/assets/tailwind/application.css")
|
|
74
|
+
return unless File.exist?(tailwind_entrypoint)
|
|
16
75
|
|
|
17
76
|
import_line = '@import "../builds/tailwind/js_admin";'
|
|
18
|
-
return if
|
|
77
|
+
return if File.read(tailwind_entrypoint).include?(import_line)
|
|
19
78
|
|
|
20
79
|
append_to_file tailwind_entrypoint, "\n#{import_line}\n"
|
|
21
80
|
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def application_path(relative_path)
|
|
85
|
+
File.join(destination_root, relative_path)
|
|
86
|
+
end
|
|
22
87
|
end
|
|
23
88
|
end
|
|
24
89
|
end
|
|
@@ -1,14 +1,29 @@
|
|
|
1
1
|
module JSAdmin
|
|
2
2
|
class Configuration
|
|
3
|
-
attr_accessor :included_models,
|
|
4
|
-
|
|
3
|
+
attr_accessor :included_models,
|
|
4
|
+
:excluded_models,
|
|
5
|
+
:records_per_page,
|
|
6
|
+
:association_limit,
|
|
7
|
+
:display_name_methods,
|
|
8
|
+
:field_select_options,
|
|
9
|
+
:basic_auth_username,
|
|
10
|
+
:basic_auth_password,
|
|
11
|
+
:basic_auth_realm
|
|
12
|
+
|
|
13
|
+
attr_reader :authentication_callback, :authorization_callback
|
|
5
14
|
|
|
6
15
|
def initialize
|
|
7
16
|
@included_models = []
|
|
8
17
|
@excluded_models = []
|
|
9
|
-
@records_per_page =
|
|
18
|
+
@records_per_page = 10
|
|
10
19
|
@association_limit = 100
|
|
11
20
|
@display_name_methods = %i[name title email username id]
|
|
21
|
+
@field_select_options = {}
|
|
22
|
+
@basic_auth_username = nil
|
|
23
|
+
@basic_auth_password = nil
|
|
24
|
+
@basic_auth_realm = "JSAdmin"
|
|
25
|
+
@authentication_callback = nil
|
|
26
|
+
@authorization_callback = nil
|
|
12
27
|
end
|
|
13
28
|
|
|
14
29
|
def include_models(*models)
|
|
@@ -19,8 +34,72 @@ module JSAdmin
|
|
|
19
34
|
excluded_models.concat(normalize_model_names(models))
|
|
20
35
|
end
|
|
21
36
|
|
|
37
|
+
def select_options(model, **options_by_field)
|
|
38
|
+
model_options = field_select_options[normalize_model_names([ model ]).first] ||= {}
|
|
39
|
+
model_options.merge!(options_by_field.transform_keys(&:to_s))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def select_options_for(model, field_name)
|
|
43
|
+
field_select_options.dig(normalize_model_names([ model ]).first, field_name.to_s)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def basic_auth_enabled?
|
|
47
|
+
basic_auth_username_configured? && basic_auth_password_configured?
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def authenticate_with(&callback)
|
|
51
|
+
@authentication_callback = callback
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def authorize_with(&callback)
|
|
55
|
+
@authorization_callback = callback
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def authentication_strategy
|
|
59
|
+
validate!
|
|
60
|
+
|
|
61
|
+
return :callback if authentication_callback
|
|
62
|
+
return :basic_auth if basic_auth_enabled?
|
|
63
|
+
|
|
64
|
+
:none
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def validate!
|
|
68
|
+
validate_basic_auth_credentials!
|
|
69
|
+
validate_authentication_strategy!
|
|
70
|
+
validate_authorization_callback!
|
|
71
|
+
|
|
72
|
+
self
|
|
73
|
+
end
|
|
74
|
+
|
|
22
75
|
private
|
|
23
76
|
|
|
77
|
+
def validate_basic_auth_credentials!
|
|
78
|
+
return if basic_auth_username_configured? == basic_auth_password_configured?
|
|
79
|
+
|
|
80
|
+
raise ConfigurationError, I18n.t("js_admin.configuration.errors.incomplete_basic_auth")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def validate_authentication_strategy!
|
|
84
|
+
return unless basic_auth_enabled? && authentication_callback
|
|
85
|
+
|
|
86
|
+
raise ConfigurationError, I18n.t("js_admin.configuration.errors.multiple_authentication_strategies")
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def validate_authorization_callback!
|
|
90
|
+
return unless authorization_callback && !authentication_callback
|
|
91
|
+
|
|
92
|
+
raise ConfigurationError, I18n.t("js_admin.configuration.errors.authorization_without_authentication")
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def basic_auth_username_configured?
|
|
96
|
+
basic_auth_username.to_s != ""
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def basic_auth_password_configured?
|
|
100
|
+
basic_auth_password.to_s != ""
|
|
101
|
+
end
|
|
102
|
+
|
|
24
103
|
def normalize_model_names(models)
|
|
25
104
|
models.flatten.map { |model| model.is_a?(Class) ? model.name : model.to_s }
|
|
26
105
|
end
|
data/lib/js_admin/field.rb
CHANGED
|
@@ -36,6 +36,7 @@ module JSAdmin
|
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def input_kind
|
|
39
|
+
return :configured_select if configured_select?
|
|
39
40
|
return :association_select if belongs_to_association
|
|
40
41
|
return :enum_select if enum?
|
|
41
42
|
|
|
@@ -65,10 +66,19 @@ module JSAdmin
|
|
|
65
66
|
resource.model_class.defined_enums.fetch(name, {}).keys
|
|
66
67
|
end
|
|
67
68
|
|
|
69
|
+
def configured_select?
|
|
70
|
+
!configured_select_source.nil?
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def configured_select_values
|
|
74
|
+
source = configured_select_source
|
|
75
|
+
Array(source.respond_to?(:call) ? source.call : source)
|
|
76
|
+
end
|
|
77
|
+
|
|
68
78
|
def belongs_to_association
|
|
69
79
|
@belongs_to_association ||= resource.model_class
|
|
70
|
-
|
|
71
|
-
|
|
80
|
+
.reflect_on_all_associations(:belongs_to)
|
|
81
|
+
.find { |association| association.foreign_key.to_s == name }
|
|
72
82
|
rescue NameError
|
|
73
83
|
nil
|
|
74
84
|
end
|
|
@@ -79,8 +89,8 @@ module JSAdmin
|
|
|
79
89
|
|
|
80
90
|
association_resource = Resource.new(association.klass)
|
|
81
91
|
association.klass
|
|
82
|
-
|
|
83
|
-
|
|
92
|
+
.limit(JSAdmin.configuration.association_limit)
|
|
93
|
+
.map { |record| [ association_resource.display_name(record), record.id ] }
|
|
84
94
|
rescue ActiveRecord::ActiveRecordError, NameError
|
|
85
95
|
[]
|
|
86
96
|
end
|
|
@@ -98,11 +108,11 @@ module JSAdmin
|
|
|
98
108
|
|
|
99
109
|
case value
|
|
100
110
|
when nil
|
|
101
|
-
"
|
|
111
|
+
I18n.t("js_admin.values.null")
|
|
102
112
|
when true
|
|
103
|
-
"true"
|
|
113
|
+
I18n.t("js_admin.values.true")
|
|
104
114
|
when false
|
|
105
|
-
"false"
|
|
115
|
+
I18n.t("js_admin.values.false")
|
|
106
116
|
else
|
|
107
117
|
value.to_s
|
|
108
118
|
end
|
|
@@ -110,6 +120,10 @@ module JSAdmin
|
|
|
110
120
|
|
|
111
121
|
private
|
|
112
122
|
|
|
123
|
+
def configured_select_source
|
|
124
|
+
JSAdmin.configuration.select_options_for(resource.model_class, name)
|
|
125
|
+
end
|
|
126
|
+
|
|
113
127
|
def inheritance_column?
|
|
114
128
|
name == resource.model_class.inheritance_column
|
|
115
129
|
end
|
data/lib/js_admin/registry.rb
CHANGED
|
@@ -7,7 +7,10 @@ module JSAdmin
|
|
|
7
7
|
|
|
8
8
|
def resource_for!(model_id)
|
|
9
9
|
resources.find { |resource| resource.model_id == model_id } ||
|
|
10
|
-
raise(
|
|
10
|
+
raise(
|
|
11
|
+
ActiveRecord::RecordNotFound,
|
|
12
|
+
I18n.t("js_admin.errors.unknown_resource", model_id: model_id)
|
|
13
|
+
)
|
|
11
14
|
end
|
|
12
15
|
|
|
13
16
|
def reset!
|