rsb-admin 0.9.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/LICENSE +15 -0
- data/README.md +83 -0
- data/Rakefile +25 -0
- data/app/assets/javascripts/rsb/admin/themes/modern.js +37 -0
- data/app/assets/stylesheets/rsb/admin/themes/default.css +1358 -0
- data/app/assets/stylesheets/rsb/admin/themes/modern.css +1370 -0
- data/app/controllers/concerns/rsb/admin/authorization.rb +21 -0
- data/app/controllers/rsb/admin/admin_controller.rb +138 -0
- data/app/controllers/rsb/admin/admin_users_controller.rb +110 -0
- data/app/controllers/rsb/admin/dashboard_controller.rb +76 -0
- data/app/controllers/rsb/admin/profile_controller.rb +146 -0
- data/app/controllers/rsb/admin/profile_sessions_controller.rb +45 -0
- data/app/controllers/rsb/admin/resources_controller.rb +386 -0
- data/app/controllers/rsb/admin/roles_controller.rb +99 -0
- data/app/controllers/rsb/admin/sessions_controller.rb +139 -0
- data/app/controllers/rsb/admin/settings_controller.rb +203 -0
- data/app/controllers/rsb/admin/two_factor_controller.rb +105 -0
- data/app/helpers/rsb/admin/authorization_helper.rb +49 -0
- data/app/helpers/rsb/admin/branding_helper.rb +38 -0
- data/app/helpers/rsb/admin/formatting_helper.rb +205 -0
- data/app/helpers/rsb/admin/i18n_helper.rb +148 -0
- data/app/helpers/rsb/admin/icons_helper.rb +55 -0
- data/app/helpers/rsb/admin/table_helper.rb +132 -0
- data/app/helpers/rsb/admin/theme_helper.rb +84 -0
- data/app/helpers/rsb/admin/url_helper.rb +109 -0
- data/app/mailers/rsb/admin/admin_mailer.rb +37 -0
- data/app/models/rsb/admin/admin_session.rb +109 -0
- data/app/models/rsb/admin/admin_user.rb +153 -0
- data/app/models/rsb/admin/application_record.rb +10 -0
- data/app/models/rsb/admin/role.rb +63 -0
- data/app/views/layouts/rsb/admin/application.html.erb +45 -0
- data/app/views/rsb/admin/admin_mailer/email_verification.html.erb +11 -0
- data/app/views/rsb/admin/admin_mailer/email_verification.text.erb +11 -0
- data/app/views/rsb/admin/admin_users/_form.html.erb +52 -0
- data/app/views/rsb/admin/admin_users/edit.html.erb +10 -0
- data/app/views/rsb/admin/admin_users/index.html.erb +77 -0
- data/app/views/rsb/admin/admin_users/new.html.erb +10 -0
- data/app/views/rsb/admin/admin_users/show.html.erb +85 -0
- data/app/views/rsb/admin/dashboard/index.html.erb +36 -0
- data/app/views/rsb/admin/profile/edit.html.erb +67 -0
- data/app/views/rsb/admin/profile/show.html.erb +155 -0
- data/app/views/rsb/admin/resources/_filters.html.erb +58 -0
- data/app/views/rsb/admin/resources/_form.html.erb +20 -0
- data/app/views/rsb/admin/resources/_pagination.html.erb +33 -0
- data/app/views/rsb/admin/resources/_table.html.erb +70 -0
- data/app/views/rsb/admin/resources/edit.html.erb +7 -0
- data/app/views/rsb/admin/resources/index.html.erb +49 -0
- data/app/views/rsb/admin/resources/new.html.erb +7 -0
- data/app/views/rsb/admin/resources/page.html.erb +9 -0
- data/app/views/rsb/admin/resources/show.html.erb +55 -0
- data/app/views/rsb/admin/roles/_form.html.erb +197 -0
- data/app/views/rsb/admin/roles/edit.html.erb +7 -0
- data/app/views/rsb/admin/roles/index.html.erb +71 -0
- data/app/views/rsb/admin/roles/new.html.erb +7 -0
- data/app/views/rsb/admin/roles/show.html.erb +99 -0
- data/app/views/rsb/admin/sessions/new.html.erb +31 -0
- data/app/views/rsb/admin/sessions/two_factor.html.erb +39 -0
- data/app/views/rsb/admin/settings/_field.html.erb +115 -0
- data/app/views/rsb/admin/settings/index.html.erb +61 -0
- data/app/views/rsb/admin/shared/_badge.html.erb +1 -0
- data/app/views/rsb/admin/shared/_breadcrumbs.html.erb +12 -0
- data/app/views/rsb/admin/shared/_empty_state.html.erb +4 -0
- data/app/views/rsb/admin/shared/_flash.html.erb +22 -0
- data/app/views/rsb/admin/shared/_header.html.erb +50 -0
- data/app/views/rsb/admin/shared/_page_tabs.html.erb +21 -0
- data/app/views/rsb/admin/shared/_sidebar.html.erb +99 -0
- data/app/views/rsb/admin/shared/disabled.html.erb +38 -0
- data/app/views/rsb/admin/shared/fields/_checkbox.html.erb +6 -0
- data/app/views/rsb/admin/shared/fields/_datetime.html.erb +10 -0
- data/app/views/rsb/admin/shared/fields/_email.html.erb +10 -0
- data/app/views/rsb/admin/shared/fields/_hidden.html.erb +1 -0
- data/app/views/rsb/admin/shared/fields/_json.html.erb +11 -0
- data/app/views/rsb/admin/shared/fields/_number.html.erb +10 -0
- data/app/views/rsb/admin/shared/fields/_password.html.erb +10 -0
- data/app/views/rsb/admin/shared/fields/_select.html.erb +12 -0
- data/app/views/rsb/admin/shared/fields/_text.html.erb +10 -0
- data/app/views/rsb/admin/shared/fields/_textarea.html.erb +10 -0
- data/app/views/rsb/admin/shared/forbidden.html.erb +22 -0
- data/app/views/rsb/admin/themes/modern/views/shared/_header.html.erb +77 -0
- data/app/views/rsb/admin/themes/modern/views/shared/_sidebar.html.erb +135 -0
- data/app/views/rsb/admin/two_factor/backup_codes.html.erb +48 -0
- data/app/views/rsb/admin/two_factor/new.html.erb +53 -0
- data/config/locales/en.yml +140 -0
- data/config/locales/seo.en.yml +21 -0
- data/config/routes.rb +59 -0
- data/db/migrate/20260208000003_create_rsb_admin_tables.rb +43 -0
- data/db/migrate/20260214000001_add_otp_fields_to_rsb_admin_admin_users.rb +9 -0
- data/lib/generators/rsb/admin/install/install_generator.rb +45 -0
- data/lib/generators/rsb/admin/install/templates/rsb_admin_seeds.rb +24 -0
- data/lib/generators/rsb/admin/theme/templates/theme.css.tt +66 -0
- data/lib/generators/rsb/admin/theme/theme_generator.rb +218 -0
- data/lib/generators/rsb/admin/views/views_generator.rb +262 -0
- data/lib/rsb/admin/breadcrumb_item.rb +26 -0
- data/lib/rsb/admin/category_registration.rb +177 -0
- data/lib/rsb/admin/column_definition.rb +89 -0
- data/lib/rsb/admin/configuration.rb +69 -0
- data/lib/rsb/admin/engine.rb +34 -0
- data/lib/rsb/admin/filter_definition.rb +129 -0
- data/lib/rsb/admin/form_field_definition.rb +96 -0
- data/lib/rsb/admin/icons.rb +95 -0
- data/lib/rsb/admin/page_registration.rb +140 -0
- data/lib/rsb/admin/registry.rb +109 -0
- data/lib/rsb/admin/resource_dsl_context.rb +139 -0
- data/lib/rsb/admin/resource_registration.rb +287 -0
- data/lib/rsb/admin/settings_schema.rb +60 -0
- data/lib/rsb/admin/test_kit/helpers.rb +316 -0
- data/lib/rsb/admin/test_kit/resource_test_case.rb +193 -0
- data/lib/rsb/admin/test_kit.rb +11 -0
- data/lib/rsb/admin/theme_definition.rb +46 -0
- data/lib/rsb/admin/themes/modern.rb +44 -0
- data/lib/rsb/admin/version.rb +9 -0
- data/lib/rsb/admin.rb +177 -0
- data/lib/tasks/rsb/admin_tasks.rake +23 -0
- metadata +227 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
<div class="flex justify-between items-center mb-6">
|
|
2
|
+
<h1 class="text-2xl font-bold"><%= rsb_admin_t("profile.title") %></h1>
|
|
3
|
+
<a href="<%= rsb_admin.edit_profile_path %>"
|
|
4
|
+
class="inline-flex items-center gap-1 px-4 py-2 border border-rsb-border rounded-rsb text-sm hover:bg-rsb-bg">
|
|
5
|
+
<%= rsb_admin_icon("edit", size: 16) %>
|
|
6
|
+
<%= rsb_admin_t("profile.edit_button") %>
|
|
7
|
+
</a>
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<div class="bg-rsb-card border border-rsb-border rounded-rsb-lg shadow-rsb-sm p-6">
|
|
11
|
+
<div class="grid grid-cols-[200px_1fr] gap-4 py-3 border-b border-rsb-border">
|
|
12
|
+
<strong class="text-sm text-rsb-muted"><%= rsb_admin_t("profile.current_email") %></strong>
|
|
13
|
+
<div><%= @admin_user.email %></div>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<% if @admin_user.email_verification_pending? %>
|
|
17
|
+
<div class="grid grid-cols-[200px_1fr] gap-4 py-3 border-b border-rsb-border">
|
|
18
|
+
<strong class="text-sm text-rsb-muted"><%= rsb_admin_t("profile.pending_email") %></strong>
|
|
19
|
+
<div class="flex items-center gap-3">
|
|
20
|
+
<span class="text-rsb-warning"><%= @admin_user.pending_email %></span>
|
|
21
|
+
<span class="text-xs text-rsb-muted">
|
|
22
|
+
<%= rsb_admin_t("profile.verification_sent_ago", time: time_ago_in_words(@admin_user.email_verification_sent_at)) %>
|
|
23
|
+
</span>
|
|
24
|
+
<%= button_to rsb_admin_t("profile.resend_verification"), rsb_admin.resend_verification_profile_path,
|
|
25
|
+
method: :post,
|
|
26
|
+
class: "text-xs text-rsb-primary hover:underline" %>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
<% end %>
|
|
30
|
+
|
|
31
|
+
<div class="grid grid-cols-[200px_1fr] gap-4 py-3 border-b border-rsb-border">
|
|
32
|
+
<strong class="text-sm text-rsb-muted"><%= rsb_admin_t("profile.current_role") %></strong>
|
|
33
|
+
<div>
|
|
34
|
+
<% if @admin_user.role %>
|
|
35
|
+
<%= @admin_user.role.name %>
|
|
36
|
+
<% if @admin_user.role.superadmin? %>
|
|
37
|
+
<%= rsb_admin_badge(rsb_admin_t("roles.superadmin"), variant: :success) %>
|
|
38
|
+
<% end %>
|
|
39
|
+
<% else %>
|
|
40
|
+
<span class="text-rsb-muted"><%= rsb_admin_t("admin_users.no_role") %></span>
|
|
41
|
+
<% end %>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div class="grid grid-cols-[200px_1fr] gap-4 py-3 border-b border-rsb-border">
|
|
46
|
+
<strong class="text-sm text-rsb-muted"><%= rsb_admin_t("profile.last_sign_in") %></strong>
|
|
47
|
+
<div>
|
|
48
|
+
<% if @admin_user.last_sign_in_at %>
|
|
49
|
+
<%= @admin_user.last_sign_in_at.strftime("%B %d, %Y at %I:%M %p") %>
|
|
50
|
+
<% else %>
|
|
51
|
+
<span class="text-rsb-muted"><%= rsb_admin_t("admin_users.never_signed_in") %></span>
|
|
52
|
+
<% end %>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<div class="grid grid-cols-[200px_1fr] gap-4 py-3">
|
|
57
|
+
<strong class="text-sm text-rsb-muted"><%= rsb_admin_t("profile.last_ip") %></strong>
|
|
58
|
+
<div><%= @admin_user.last_sign_in_ip || "-" %></div>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<%# ── Two-Factor Authentication ─────────────────────────────── %>
|
|
63
|
+
<div class="bg-rsb-card border border-rsb-border rounded-rsb-lg shadow-rsb-sm p-6 mt-6">
|
|
64
|
+
<h3 class="text-lg font-semibold mb-4">Two-Factor Authentication</h3>
|
|
65
|
+
|
|
66
|
+
<% if @admin_user.otp_enabled? %>
|
|
67
|
+
<div class="flex items-center justify-between">
|
|
68
|
+
<div class="flex items-center gap-2">
|
|
69
|
+
<%= rsb_admin_icon("shield-check", css_class: "text-rsb-success") %>
|
|
70
|
+
<span class="font-medium">Enabled</span>
|
|
71
|
+
<span class="text-xs text-rsb-muted">(TOTP)</span>
|
|
72
|
+
</div>
|
|
73
|
+
<div>
|
|
74
|
+
<%= form_tag(rsb_admin.profile_two_factor_path, method: :delete, class: "inline") do %>
|
|
75
|
+
<div class="flex items-center gap-2">
|
|
76
|
+
<input type="password" name="current_password" placeholder="Current password"
|
|
77
|
+
class="px-2 py-1 text-sm border border-rsb-border rounded-rsb bg-rsb-bg" required />
|
|
78
|
+
<button type="submit"
|
|
79
|
+
class="px-3 py-1 text-sm border border-rsb-danger text-rsb-danger rounded-rsb hover:bg-rsb-danger-bg transition-colors"
|
|
80
|
+
onclick="return confirm('Are you sure you want to disable two-factor authentication?')">
|
|
81
|
+
Disable 2FA
|
|
82
|
+
</button>
|
|
83
|
+
</div>
|
|
84
|
+
<% end %>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
<% else %>
|
|
88
|
+
<div class="flex items-center justify-between">
|
|
89
|
+
<div class="flex items-center gap-2">
|
|
90
|
+
<%= rsb_admin_icon("shield-off", css_class: "text-rsb-muted") %>
|
|
91
|
+
<span class="text-rsb-muted">Not set up</span>
|
|
92
|
+
</div>
|
|
93
|
+
<a href="<%= rsb_admin.new_profile_two_factor_path %>"
|
|
94
|
+
class="px-3 py-1 text-sm bg-rsb-primary text-rsb-primary-text rounded-rsb hover:bg-rsb-primary-hover">
|
|
95
|
+
Enable 2FA
|
|
96
|
+
</a>
|
|
97
|
+
</div>
|
|
98
|
+
<% end %>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<%# ── Active Sessions ─────────────────────────────── %>
|
|
102
|
+
<div class="mt-6">
|
|
103
|
+
<div class="flex justify-between items-center mb-4">
|
|
104
|
+
<h2 class="text-lg font-semibold"><%= rsb_admin_t("profile.sessions_title") %></h2>
|
|
105
|
+
<% if @admin_user.admin_sessions.count > 1 %>
|
|
106
|
+
<%= button_to rsb_admin_t("profile.revoke_all_sessions"),
|
|
107
|
+
rsb_admin.profile_sessions_path,
|
|
108
|
+
method: :delete,
|
|
109
|
+
data: { turbo_confirm: rsb_admin_t("profile.confirm_revoke_all") },
|
|
110
|
+
class: "text-sm text-rsb-danger hover:underline" %>
|
|
111
|
+
<% end %>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<div class="bg-rsb-card border border-rsb-border rounded-rsb-lg shadow-rsb-sm divide-y divide-rsb-border">
|
|
115
|
+
<% @admin_user.admin_sessions.order(last_active_at: :desc).each do |admin_session| %>
|
|
116
|
+
<% is_current = admin_session.current?(session[:rsb_admin_session_token]) %>
|
|
117
|
+
<div class="flex items-center justify-between px-6 py-4">
|
|
118
|
+
<div class="flex items-center gap-4">
|
|
119
|
+
<div class="text-rsb-muted">
|
|
120
|
+
<% icon_name = case admin_session.device_type
|
|
121
|
+
when "mobile" then "smartphone"
|
|
122
|
+
when "tablet" then "tablet"
|
|
123
|
+
else "monitor"
|
|
124
|
+
end %>
|
|
125
|
+
<%= rsb_admin_icon(icon_name, size: 20) %>
|
|
126
|
+
</div>
|
|
127
|
+
<div>
|
|
128
|
+
<div class="text-sm font-medium">
|
|
129
|
+
<%= admin_session.browser %> <%= rsb_admin_t("profile.on") %> <%= admin_session.os %>
|
|
130
|
+
<% if is_current %>
|
|
131
|
+
<%= rsb_admin_badge(rsb_admin_t("profile.current_session"), variant: :success) %>
|
|
132
|
+
<% end %>
|
|
133
|
+
</div>
|
|
134
|
+
<div class="text-xs text-rsb-muted mt-0.5">
|
|
135
|
+
<%= admin_session.ip_address %> ·
|
|
136
|
+
<% if is_current %>
|
|
137
|
+
<%= rsb_admin_t("profile.active_now") %>
|
|
138
|
+
<% else %>
|
|
139
|
+
<%= rsb_admin_t("profile.last_active", time: time_ago_in_words(admin_session.last_active_at)) %>
|
|
140
|
+
<% end %>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
<div>
|
|
145
|
+
<% unless is_current %>
|
|
146
|
+
<%= button_to rsb_admin_t("profile.revoke_session"),
|
|
147
|
+
rsb_admin.profile_session_path(admin_session),
|
|
148
|
+
method: :delete,
|
|
149
|
+
class: "text-sm text-rsb-danger hover:underline" %>
|
|
150
|
+
<% end %>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
<% end %>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<div class="bg-rsb-card border border-rsb-border rounded-rsb-lg p-4 mb-4">
|
|
2
|
+
<form method="get" action="<%= request.path %>" class="flex flex-wrap items-end gap-4">
|
|
3
|
+
<% filters.each do |filter| %>
|
|
4
|
+
<div>
|
|
5
|
+
<label class="block text-xs font-medium text-rsb-muted mb-1">
|
|
6
|
+
<%= rsb_admin_filter_label(filter, resource_key: resource_key) %>
|
|
7
|
+
</label>
|
|
8
|
+
<% case filter.type %>
|
|
9
|
+
<% when :text %>
|
|
10
|
+
<input type="text" name="q[<%= filter.key %>]" value="<%= values[filter.key.to_s] %>"
|
|
11
|
+
class="px-3 py-1.5 border border-rsb-border rounded-rsb text-sm focus:outline-none focus:border-rsb-primary focus:ring-2 focus:ring-rsb-primary/10"
|
|
12
|
+
placeholder="<%= rsb_admin_filter_label(filter, resource_key: resource_key) %>...">
|
|
13
|
+
<% when :select %>
|
|
14
|
+
<select name="q[<%= filter.key %>]"
|
|
15
|
+
class="px-3 py-1.5 border border-rsb-border rounded-rsb text-sm focus:outline-none focus:border-rsb-primary focus:ring-2 focus:ring-rsb-primary/10">
|
|
16
|
+
<option value=""><%= rsb_admin_t("shared.all") %></option>
|
|
17
|
+
<% opts = filter.options.is_a?(Proc) ? filter.options.call : filter.options %>
|
|
18
|
+
<% opts&.each do |opt| %>
|
|
19
|
+
<option value="<%= opt %>" <%= "selected" if values[filter.key.to_s] == opt.to_s %>><%= opt.to_s.titleize %></option>
|
|
20
|
+
<% end %>
|
|
21
|
+
</select>
|
|
22
|
+
<% when :boolean %>
|
|
23
|
+
<select name="q[<%= filter.key %>]"
|
|
24
|
+
class="px-3 py-1.5 border border-rsb-border rounded-rsb text-sm focus:outline-none focus:border-rsb-primary focus:ring-2 focus:ring-rsb-primary/10">
|
|
25
|
+
<option value=""><%= rsb_admin_t("shared.all") %></option>
|
|
26
|
+
<option value="true" <%= "selected" if values[filter.key.to_s] == "true" %>>Yes</option>
|
|
27
|
+
<option value="false" <%= "selected" if values[filter.key.to_s] == "false" %>>No</option>
|
|
28
|
+
</select>
|
|
29
|
+
<% when :date_range %>
|
|
30
|
+
<div class="flex gap-2">
|
|
31
|
+
<input type="date" name="q[<%= filter.key %>][from]" value="<%= values.dig(filter.key.to_s, "from") %>"
|
|
32
|
+
class="px-3 py-1.5 border border-rsb-border rounded-rsb text-sm">
|
|
33
|
+
<input type="date" name="q[<%= filter.key %>][to]" value="<%= values.dig(filter.key.to_s, "to") %>"
|
|
34
|
+
class="px-3 py-1.5 border border-rsb-border rounded-rsb text-sm">
|
|
35
|
+
</div>
|
|
36
|
+
<% when :number_range %>
|
|
37
|
+
<div class="flex gap-2">
|
|
38
|
+
<input type="number" name="q[<%= filter.key %>][min]" value="<%= values.dig(filter.key.to_s, "min") %>"
|
|
39
|
+
class="px-3 py-1.5 border border-rsb-border rounded-rsb text-sm w-24" placeholder="Min">
|
|
40
|
+
<input type="number" name="q[<%= filter.key %>][max]" value="<%= values.dig(filter.key.to_s, "max") %>"
|
|
41
|
+
class="px-3 py-1.5 border border-rsb-border rounded-rsb text-sm w-24" placeholder="Max">
|
|
42
|
+
</div>
|
|
43
|
+
<% end %>
|
|
44
|
+
</div>
|
|
45
|
+
<% end %>
|
|
46
|
+
<div class="flex gap-2">
|
|
47
|
+
<button type="submit"
|
|
48
|
+
class="inline-flex items-center gap-1 px-4 py-1.5 bg-rsb-primary text-rsb-primary-text rounded-rsb text-sm font-medium hover:bg-rsb-primary-hover">
|
|
49
|
+
<%= rsb_admin_icon("filter", size: 14) %>
|
|
50
|
+
<%= rsb_admin_t("shared.apply_filters") %>
|
|
51
|
+
</button>
|
|
52
|
+
<a href="<%= request.path %>"
|
|
53
|
+
class="px-4 py-1.5 border border-rsb-border text-rsb-muted rounded-rsb text-sm hover:bg-rsb-bg">
|
|
54
|
+
<%= rsb_admin_t("shared.clear_filters") %>
|
|
55
|
+
</a>
|
|
56
|
+
</div>
|
|
57
|
+
</form>
|
|
58
|
+
</div>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<% fields = action_name.in?(%w[new create]) ? @registration.new_form_fields : @registration.edit_form_fields %>
|
|
2
|
+
<% form_url = action_name.in?(%w[new create]) ? rsb_admin_resource_path(@registration.route_key) : rsb_admin_resource_show_path(@registration.route_key, @record.id) %>
|
|
3
|
+
<% form_method = action_name.in?(%w[new create]) ? :post : :patch %>
|
|
4
|
+
<%= form_with(model: @record, url: form_url, method: form_method, local: true) do |f| %>
|
|
5
|
+
<% fields.each do |field| %>
|
|
6
|
+
<%= render rsb_admin_partial("shared/fields/#{field.type}"),
|
|
7
|
+
field: field, form: f, resource_key: @registration.route_key %>
|
|
8
|
+
<% end %>
|
|
9
|
+
|
|
10
|
+
<div class="flex gap-3 mt-6">
|
|
11
|
+
<button type="submit"
|
|
12
|
+
class="px-4 py-2 bg-rsb-primary text-rsb-primary-text rounded-rsb text-sm font-medium hover:bg-rsb-primary-hover">
|
|
13
|
+
<%= rsb_admin_t("shared.save") %>
|
|
14
|
+
</button>
|
|
15
|
+
<a href="<%= rsb_admin_resource_path(@registration.route_key) %>"
|
|
16
|
+
class="px-4 py-2 border border-rsb-border text-rsb-text rounded-rsb text-sm hover:bg-rsb-bg">
|
|
17
|
+
<%= rsb_admin_t("shared.cancel") %>
|
|
18
|
+
</a>
|
|
19
|
+
</div>
|
|
20
|
+
<% end %>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<div class="flex justify-between items-center mt-4 pt-4 border-t border-rsb-border">
|
|
2
|
+
<div class="text-sm text-rsb-muted">
|
|
3
|
+
<% from = ((current_page - 1) * per_page) + 1 %>
|
|
4
|
+
<% to = [current_page * per_page, total_count].min %>
|
|
5
|
+
<%= rsb_admin_t("shared.showing", from: from, to: to, total: total_count) %>
|
|
6
|
+
</div>
|
|
7
|
+
<div class="flex items-center gap-1">
|
|
8
|
+
<% if current_page > 1 %>
|
|
9
|
+
<a href="<%= base_path %>?page=<%= current_page - 1 %>"
|
|
10
|
+
class="px-3 py-1.5 border border-rsb-border rounded-rsb text-sm hover:bg-rsb-bg">
|
|
11
|
+
<%= rsb_admin_t("shared.previous") %>
|
|
12
|
+
</a>
|
|
13
|
+
<% end %>
|
|
14
|
+
<% (1..total_pages).each do |page_num| %>
|
|
15
|
+
<% if page_num == current_page %>
|
|
16
|
+
<span class="px-3 py-1.5 bg-rsb-primary text-rsb-primary-text rounded-rsb text-sm font-medium">
|
|
17
|
+
<%= page_num %>
|
|
18
|
+
</span>
|
|
19
|
+
<% else %>
|
|
20
|
+
<a href="<%= base_path %>?page=<%= page_num %>"
|
|
21
|
+
class="px-3 py-1.5 border border-rsb-border rounded-rsb text-sm hover:bg-rsb-bg">
|
|
22
|
+
<%= page_num %>
|
|
23
|
+
</a>
|
|
24
|
+
<% end %>
|
|
25
|
+
<% end %>
|
|
26
|
+
<% if current_page < total_pages %>
|
|
27
|
+
<a href="<%= base_path %>?page=<%= current_page + 1 %>"
|
|
28
|
+
class="px-3 py-1.5 border border-rsb-border rounded-rsb text-sm hover:bg-rsb-bg">
|
|
29
|
+
<%= rsb_admin_t("shared.next") %>
|
|
30
|
+
</a>
|
|
31
|
+
<% end %>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<div class="overflow-x-auto">
|
|
2
|
+
<table class="w-full">
|
|
3
|
+
<thead>
|
|
4
|
+
<tr>
|
|
5
|
+
<% columns.each do |col| %>
|
|
6
|
+
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-rsb-muted bg-rsb-bg border-b border-rsb-border">
|
|
7
|
+
<% if col.sortable %>
|
|
8
|
+
<a href="<%= sort_link(col) %>" class="flex items-center gap-1 hover:text-rsb-text">
|
|
9
|
+
<%= rsb_admin_column_label(col, resource_key: registration.route_key) %>
|
|
10
|
+
<% if params[:sort] == col.key.to_s %>
|
|
11
|
+
<%= rsb_admin_icon(params[:dir] == "desc" ? "arrow-down" : "arrow-up", size: 14) %>
|
|
12
|
+
<% end %>
|
|
13
|
+
</a>
|
|
14
|
+
<% else %>
|
|
15
|
+
<%= rsb_admin_column_label(col, resource_key: registration.route_key) %>
|
|
16
|
+
<% end %>
|
|
17
|
+
</th>
|
|
18
|
+
<% end %>
|
|
19
|
+
<th class="px-4 py-3 text-right text-xs font-semibold uppercase tracking-wider text-rsb-muted bg-rsb-bg border-b border-rsb-border">
|
|
20
|
+
</th>
|
|
21
|
+
</tr>
|
|
22
|
+
</thead>
|
|
23
|
+
<tbody>
|
|
24
|
+
<% records.each do |record| %>
|
|
25
|
+
<tr class="hover:bg-rsb-bg border-b border-rsb-border last:border-b-0">
|
|
26
|
+
<% columns.each do |col| %>
|
|
27
|
+
<td class="px-4 py-3 text-sm">
|
|
28
|
+
<% value = record.respond_to?(col.key) ? record.send(col.key) : nil %>
|
|
29
|
+
<% if col.link && registration.action?(:show) %>
|
|
30
|
+
<a href="<%= rsb_admin_resource_show_path(registration.route_key, record.id) %>" class="text-rsb-primary hover:underline">
|
|
31
|
+
<%= rsb_admin_format_value(value, col.formatter, record) %>
|
|
32
|
+
</a>
|
|
33
|
+
<% else %>
|
|
34
|
+
<%= rsb_admin_format_value(value, col.formatter, record) %>
|
|
35
|
+
<% end %>
|
|
36
|
+
</td>
|
|
37
|
+
<% end %>
|
|
38
|
+
<td class="px-4 py-3 text-sm text-right">
|
|
39
|
+
<div class="flex items-center gap-2 justify-end">
|
|
40
|
+
<% if registration.action?(:show) %>
|
|
41
|
+
<% if rsb_admin_can?(registration.route_key, "show") %>
|
|
42
|
+
<a href="<%= rsb_admin_resource_show_path(registration.route_key, record.id) %>"
|
|
43
|
+
class="text-rsb-muted hover:text-rsb-text" title="<%= rsb_admin_t("shared.view") %>">
|
|
44
|
+
<%= rsb_admin_icon("eye", size: 16) %>
|
|
45
|
+
</a>
|
|
46
|
+
<% else %>
|
|
47
|
+
<span class="text-rsb-muted/40 cursor-not-allowed" title="<%= rsb_admin_t("shared.no_access") %>">
|
|
48
|
+
<%= rsb_admin_icon("eye", size: 16) %>
|
|
49
|
+
</span>
|
|
50
|
+
<% end %>
|
|
51
|
+
<% end %>
|
|
52
|
+
<% if registration.action?(:edit) %>
|
|
53
|
+
<% if rsb_admin_can?(registration.route_key, "edit") %>
|
|
54
|
+
<a href="<%= rsb_admin_resource_edit_path(registration.route_key, record.id) %>"
|
|
55
|
+
class="text-rsb-muted hover:text-rsb-text" title="<%= rsb_admin_t("shared.edit") %>">
|
|
56
|
+
<%= rsb_admin_icon("edit", size: 16) %>
|
|
57
|
+
</a>
|
|
58
|
+
<% else %>
|
|
59
|
+
<span class="text-rsb-muted/40 cursor-not-allowed" title="<%= rsb_admin_t("shared.no_access") %>">
|
|
60
|
+
<%= rsb_admin_icon("edit", size: 16) %>
|
|
61
|
+
</span>
|
|
62
|
+
<% end %>
|
|
63
|
+
<% end %>
|
|
64
|
+
</div>
|
|
65
|
+
</td>
|
|
66
|
+
</tr>
|
|
67
|
+
<% end %>
|
|
68
|
+
</tbody>
|
|
69
|
+
</table>
|
|
70
|
+
</div>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<div class="flex justify-between items-center mb-6">
|
|
2
|
+
<h1 class="text-2xl font-bold"><%= rsb_admin_t("shared.edit") %> <%= @registration.label.singularize %> #<%= @record.id %></h1>
|
|
3
|
+
</div>
|
|
4
|
+
|
|
5
|
+
<div class="bg-rsb-card border border-rsb-border rounded-rsb-lg shadow-rsb-sm p-6">
|
|
6
|
+
<%= render rsb_admin_partial("resources/form") %>
|
|
7
|
+
</div>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<div class="flex justify-between items-center mb-6">
|
|
2
|
+
<h1 class="text-2xl font-bold"><%= @registration.label %></h1>
|
|
3
|
+
<div class="flex gap-2">
|
|
4
|
+
<% if @registration.action?(:new) %>
|
|
5
|
+
<% if rsb_admin_can?(@registration.route_key, "new") %>
|
|
6
|
+
<a href="<%= rsb_admin_resource_new_path(@registration.route_key) %>"
|
|
7
|
+
class="inline-flex items-center gap-1 px-4 py-2 bg-rsb-primary text-rsb-primary-text rounded-rsb text-sm font-medium hover:bg-rsb-primary-hover transition-colors">
|
|
8
|
+
<%= rsb_admin_icon("plus", size: 16) %>
|
|
9
|
+
<%= rsb_admin_t("shared.new", resource: @registration.label.singularize) %>
|
|
10
|
+
</a>
|
|
11
|
+
<% else %>
|
|
12
|
+
<span class="inline-flex items-center gap-1 px-4 py-2 bg-rsb-primary/50 text-rsb-primary-text/50 rounded-rsb text-sm font-medium pointer-events-none cursor-not-allowed"
|
|
13
|
+
title="<%= rsb_admin_t("shared.no_access") %>">
|
|
14
|
+
<%= rsb_admin_icon("plus", size: 16) %>
|
|
15
|
+
<%= rsb_admin_t("shared.new", resource: @registration.label.singularize) %>
|
|
16
|
+
</span>
|
|
17
|
+
<% end %>
|
|
18
|
+
<% end %>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<% if @registration.filters&.any? %>
|
|
23
|
+
<%= render rsb_admin_partial("resources/filters"),
|
|
24
|
+
filters: @registration.filters,
|
|
25
|
+
values: @filter_values,
|
|
26
|
+
resource_key: @registration.route_key %>
|
|
27
|
+
<% end %>
|
|
28
|
+
|
|
29
|
+
<div class="bg-rsb-card border border-rsb-border rounded-rsb-lg shadow-rsb-sm overflow-hidden">
|
|
30
|
+
<% if @records.any? %>
|
|
31
|
+
<%= render rsb_admin_partial("resources/table"),
|
|
32
|
+
columns: @registration.index_columns,
|
|
33
|
+
records: @records,
|
|
34
|
+
registration: @registration %>
|
|
35
|
+
<% else %>
|
|
36
|
+
<%= render rsb_admin_partial("shared/empty_state"),
|
|
37
|
+
title: rsb_admin_t("shared.no_results", resource: @registration.label),
|
|
38
|
+
message: rsb_admin_t("shared.no_results_message", resource: @registration.label) %>
|
|
39
|
+
<% end %>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<% if @total_pages > 1 %>
|
|
43
|
+
<%= render rsb_admin_partial("resources/pagination"),
|
|
44
|
+
current_page: @current_page,
|
|
45
|
+
total_pages: @total_pages,
|
|
46
|
+
total_count: @total_count,
|
|
47
|
+
per_page: @registration.per_page || RSB::Admin.configuration.per_page,
|
|
48
|
+
base_path: rsb_admin_resource_path(@registration.route_key) %>
|
|
49
|
+
<% end %>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<div class="flex justify-between items-center mb-6">
|
|
2
|
+
<h1 class="text-2xl font-bold"><%= rsb_admin_t("shared.new", resource: @registration.label.singularize) %></h1>
|
|
3
|
+
</div>
|
|
4
|
+
|
|
5
|
+
<div class="bg-rsb-card border border-rsb-border rounded-rsb-lg shadow-rsb-sm p-6">
|
|
6
|
+
<%= render rsb_admin_partial("resources/form") %>
|
|
7
|
+
</div>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<div class="flex justify-between items-center mb-6">
|
|
2
|
+
<h1 class="text-2xl font-bold"><%= @page.label %></h1>
|
|
3
|
+
</div>
|
|
4
|
+
|
|
5
|
+
<div class="bg-rsb-card border border-rsb-border rounded-rsb-lg shadow-rsb-sm p-6">
|
|
6
|
+
<p class="text-rsb-muted text-sm">
|
|
7
|
+
<%= rsb_admin_t("resources.page_placeholder", controller: @page.controller) %>
|
|
8
|
+
</p>
|
|
9
|
+
</div>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<div class="flex justify-between items-center mb-6">
|
|
2
|
+
<h1 class="text-2xl font-bold"><%= @registration.label.singularize %> #<%= @record.id %></h1>
|
|
3
|
+
<div class="flex gap-2">
|
|
4
|
+
<% if @registration.action?(:edit) %>
|
|
5
|
+
<% if rsb_admin_can?(@registration.route_key, "edit") %>
|
|
6
|
+
<a href="<%= rsb_admin_resource_edit_path(@registration.route_key, @record.id) %>"
|
|
7
|
+
class="inline-flex items-center gap-1 px-4 py-2 border border-rsb-border rounded-rsb text-sm hover:bg-rsb-bg">
|
|
8
|
+
<%= rsb_admin_icon("edit", size: 16) %>
|
|
9
|
+
<%= rsb_admin_t("shared.edit") %>
|
|
10
|
+
</a>
|
|
11
|
+
<% else %>
|
|
12
|
+
<span class="inline-flex items-center gap-1 px-4 py-2 border border-rsb-border/50 rounded-rsb text-sm text-rsb-muted pointer-events-none cursor-not-allowed"
|
|
13
|
+
title="<%= rsb_admin_t("shared.no_access") %>">
|
|
14
|
+
<%= rsb_admin_icon("edit", size: 16) %>
|
|
15
|
+
<%= rsb_admin_t("shared.edit") %>
|
|
16
|
+
</span>
|
|
17
|
+
<% end %>
|
|
18
|
+
<% end %>
|
|
19
|
+
<a href="<%= rsb_admin_resource_path(@registration.route_key) %>"
|
|
20
|
+
class="inline-flex items-center gap-1 px-4 py-2 border border-rsb-border rounded-rsb text-sm hover:bg-rsb-bg">
|
|
21
|
+
<%= rsb_admin_t("shared.back") %>
|
|
22
|
+
</a>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div class="bg-rsb-card border border-rsb-border rounded-rsb-lg shadow-rsb-sm p-6">
|
|
27
|
+
<% @registration.show_columns.each do |col| %>
|
|
28
|
+
<% value = @record.respond_to?(col.key) ? @record.send(col.key) : nil %>
|
|
29
|
+
<div class="grid grid-cols-[200px_1fr] gap-4 py-3 border-b border-rsb-border last:border-b-0">
|
|
30
|
+
<strong class="text-sm text-rsb-muted"><%= rsb_admin_column_label(col, resource_key: @registration.route_key) %></strong>
|
|
31
|
+
<div><%= rsb_admin_format_value(value, col.formatter, @record) %></div>
|
|
32
|
+
</div>
|
|
33
|
+
<% end %>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<% if @registration.action?(:destroy) %>
|
|
37
|
+
<% if rsb_admin_can?(@registration.route_key, "destroy") %>
|
|
38
|
+
<div class="bg-rsb-card border border-rsb-border rounded-rsb-lg shadow-rsb-sm p-6 mt-4">
|
|
39
|
+
<h3 class="text-base font-semibold text-rsb-danger mb-4"><%= rsb_admin_t("shared.danger_zone") %></h3>
|
|
40
|
+
<%= button_to rsb_admin_t("shared.delete") + " " + @registration.label.singularize,
|
|
41
|
+
rsb_admin_resource_show_path(@registration.route_key, @record.id),
|
|
42
|
+
method: :delete,
|
|
43
|
+
data: { turbo_confirm: rsb_admin_t("shared.confirm_delete", resource: @registration.label.singularize.downcase) },
|
|
44
|
+
class: "px-4 py-2 bg-rsb-danger text-white rounded-rsb text-sm font-medium hover:bg-red-700" %>
|
|
45
|
+
</div>
|
|
46
|
+
<% else %>
|
|
47
|
+
<div class="bg-rsb-card border border-rsb-border rounded-rsb-lg shadow-rsb-sm p-6 mt-4">
|
|
48
|
+
<h3 class="text-base font-semibold text-rsb-danger mb-4"><%= rsb_admin_t("shared.danger_zone") %></h3>
|
|
49
|
+
<span class="inline-flex px-4 py-2 bg-rsb-danger/50 text-white/50 rounded-rsb text-sm font-medium pointer-events-none cursor-not-allowed"
|
|
50
|
+
title="<%= rsb_admin_t("shared.no_access") %>">
|
|
51
|
+
<%= rsb_admin_t("shared.delete") %> <%= @registration.label.singularize %>
|
|
52
|
+
</span>
|
|
53
|
+
</div>
|
|
54
|
+
<% end %>
|
|
55
|
+
<% end %>
|