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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +24 -0
  3. data/README.md +227 -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 +45 -1
  9. data/app/controllers/js_admin/home_controller.rb +6 -0
  10. data/app/controllers/js_admin/resources_controller.rb +21 -6
  11. data/app/helpers/js_admin/application_helper.rb +292 -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 +4 -21
  15. data/app/views/js_admin/resources/_edit_content.html.erb +11 -0
  16. data/app/views/js_admin/resources/_form.html.erb +21 -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 +4 -12
  22. data/app/views/js_admin/resources/index.html.erb +3 -52
  23. data/app/views/js_admin/resources/new.html.erb +4 -11
  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/locales/js_admin.en.yml +65 -0
  31. data/config/locales/js_admin.ko.yml +65 -0
  32. data/config/routes.rb +2 -1
  33. data/lib/generators/js_admin/install/install_generator.rb +70 -5
  34. data/lib/js_admin/configuration.rb +82 -3
  35. data/lib/js_admin/configuration_error.rb +4 -0
  36. data/lib/js_admin/field.rb +21 -7
  37. data/lib/js_admin/registry.rb +4 -1
  38. data/lib/js_admin/resource.rb +17 -5
  39. data/lib/js_admin/version.rb +1 -1
  40. data/lib/js_admin.rb +8 -0
  41. metadata +40 -2
@@ -1,54 +1,5 @@
1
1
  <% content_for :title, @resource.label %>
2
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>
3
+ <%= render layout: "layouts/js_admin/content_frame" do %>
4
+ <%= render "index_content" %>
5
+ <% end %>
@@ -1,12 +1,5 @@
1
- <% content_for :title, "New #{@resource.singular_label}" %>
1
+ <% content_for :title, t("js_admin.titles.new", resource: @resource.singular_label) %>
2
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) %>
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
- <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>
3
+ <%= render layout: "layouts/js_admin/content_frame" do %>
4
+ <%= render "show_content" %>
5
+ <% end %>
@@ -0,0 +1,7 @@
1
+ <%= turbo_frame_tag js_admin_content_frame_id, data: js_admin_content_frame_data do %>
2
+ <%= render "layouts/js_admin/flash", notice: notice, alert: alert %>
3
+
4
+ <div id="<%= js_admin_content_body_id %>">
5
+ <%= yield %>
6
+ </div>
7
+ <% end %>
@@ -0,0 +1,3 @@
1
+ <div id="<%= js_admin_flash_id %>" class="pointer-events-none fixed right-0 top-4 z-50 flex w-full justify-end px-4 sm:right-4 sm:w-auto sm:px-0">
2
+ <%= render "layouts/js_admin/flash_messages", notice: notice, alert: alert %>
3
+ </div>
@@ -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">&gt;</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
- <%= js_admin_hotwire_javascript_tags %>
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="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" %>
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
- <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 %>
28
+ <div class="flex w-full flex-col pt-16 lg:flex-row">
29
+ <%= render "layouts/js_admin/sidebar" %>
32
30
 
33
- <%= yield %>
34
- </main>
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: "models#index"
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
- class_option :path, type: :string, default: "/js_admin",
7
- desc: "Mount path for JSAdmin"
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 = Rails.root.join("app/assets/tailwind/application.css")
15
- return unless tailwind_entrypoint.exist?
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 tailwind_entrypoint.read.include?(import_line)
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, :excluded_models, :records_per_page,
4
- :association_limit, :display_name_methods
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 = 25
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
@@ -0,0 +1,4 @@
1
+ module JSAdmin
2
+ class ConfigurationError < StandardError
3
+ end
4
+ end
@@ -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
- .reflect_on_all_associations(:belongs_to)
71
- .find { |association| association.foreign_key.to_s == name }
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
- .limit(JSAdmin.configuration.association_limit)
83
- .map { |record| [ association_resource.display_name(record), record.id ] }
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
- "NULL"
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
@@ -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(ActiveRecord::RecordNotFound, "Unknown JSAdmin resource: #{model_id}")
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!