admin_suite 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/Gemfile +13 -0
- data/LICENSE.txt +22 -0
- data/README.md +7 -0
- data/Rakefile +11 -0
- data/app/assets/admin_suite.css +444 -0
- data/app/assets/admin_suite_tailwind.css +8 -0
- data/app/assets/builds/admin_suite_tailwind.css +8 -0
- data/app/assets/rouge.css +218 -0
- data/app/assets/tailwind/admin_suite.css +22 -0
- data/app/controllers/admin_suite/application_controller.rb +118 -0
- data/app/controllers/admin_suite/dashboard_controller.rb +258 -0
- data/app/controllers/admin_suite/docs_controller.rb +155 -0
- data/app/controllers/admin_suite/portals_controller.rb +22 -0
- data/app/controllers/admin_suite/resources_controller.rb +238 -0
- data/app/helpers/admin_suite/base_helper.rb +1199 -0
- data/app/helpers/admin_suite/icon_helper.rb +61 -0
- data/app/helpers/admin_suite/panels_helper.rb +52 -0
- data/app/helpers/admin_suite/resources_helper.rb +15 -0
- data/app/helpers/admin_suite/theme_helper.rb +99 -0
- data/app/javascript/controllers/admin_suite/click_actions_controller.js +73 -0
- data/app/javascript/controllers/admin_suite/clipboard_controller.js +57 -0
- data/app/javascript/controllers/admin_suite/code_editor_controller.js +45 -0
- data/app/javascript/controllers/admin_suite/file_upload_controller.js +238 -0
- data/app/javascript/controllers/admin_suite/json_editor_controller.js +62 -0
- data/app/javascript/controllers/admin_suite/live_filter_controller.js +71 -0
- data/app/javascript/controllers/admin_suite/markdown_editor_controller.js +67 -0
- data/app/javascript/controllers/admin_suite/searchable_select_controller.js +171 -0
- data/app/javascript/controllers/admin_suite/sidebar_controller.js +33 -0
- data/app/javascript/controllers/admin_suite/tag_select_controller.js +193 -0
- data/app/javascript/controllers/admin_suite/toggle_switch_controller.js +66 -0
- data/app/views/admin_suite/dashboard/index.html.erb +21 -0
- data/app/views/admin_suite/docs/index.html.erb +86 -0
- data/app/views/admin_suite/panels/_cards.html.erb +107 -0
- data/app/views/admin_suite/panels/_chart.html.erb +47 -0
- data/app/views/admin_suite/panels/_health.html.erb +44 -0
- data/app/views/admin_suite/panels/_recent.html.erb +56 -0
- data/app/views/admin_suite/panels/_stat.html.erb +64 -0
- data/app/views/admin_suite/panels/_table.html.erb +36 -0
- data/app/views/admin_suite/portals/show.html.erb +75 -0
- data/app/views/admin_suite/resources/_form.html.erb +32 -0
- data/app/views/admin_suite/resources/edit.html.erb +24 -0
- data/app/views/admin_suite/resources/index.html.erb +315 -0
- data/app/views/admin_suite/resources/new.html.erb +22 -0
- data/app/views/admin_suite/resources/show.html.erb +184 -0
- data/app/views/admin_suite/shared/_flash.html.erb +30 -0
- data/app/views/admin_suite/shared/_form.html.erb +60 -0
- data/app/views/admin_suite/shared/_json_editor_field.html.erb +52 -0
- data/app/views/admin_suite/shared/_sidebar.html.erb +94 -0
- data/app/views/admin_suite/shared/_toggle_cell.html.erb +34 -0
- data/app/views/admin_suite/shared/_topbar.html.erb +47 -0
- data/app/views/layouts/admin_suite/application.html.erb +79 -0
- data/lib/admin/base/action_executor.rb +155 -0
- data/lib/admin/base/action_handler.rb +31 -0
- data/lib/admin/base/filter_builder.rb +121 -0
- data/lib/admin/base/resource.rb +541 -0
- data/lib/admin_suite/configuration.rb +42 -0
- data/lib/admin_suite/engine.rb +101 -0
- data/lib/admin_suite/markdown_renderer.rb +115 -0
- data/lib/admin_suite/portal_definition.rb +64 -0
- data/lib/admin_suite/portal_registry.rb +32 -0
- data/lib/admin_suite/theme_palette.rb +36 -0
- data/lib/admin_suite/ui/dashboard_definition.rb +69 -0
- data/lib/admin_suite/ui/field_renderer_registry.rb +119 -0
- data/lib/admin_suite/ui/form_field_renderer.rb +48 -0
- data/lib/admin_suite/ui/show_formatter_registry.rb +120 -0
- data/lib/admin_suite/ui/show_value_formatter.rb +70 -0
- data/lib/admin_suite/version.rb +10 -0
- data/lib/admin_suite.rb +54 -0
- data/lib/generators/admin_suite/install/install_generator.rb +23 -0
- data/lib/generators/admin_suite/install/templates/admin_suite.rb +60 -0
- data/lib/generators/admin_suite/resource/resource_generator.rb +83 -0
- data/lib/generators/admin_suite/resource/templates/resource.rb.tt +47 -0
- data/lib/generators/admin_suite/scaffold/scaffold_generator.rb +28 -0
- data/lib/tasks/admin_suite_tailwind.rake +28 -0
- data/lib/tasks/admin_suite_test.rake +11 -0
- data/test/dummy/Gemfile +21 -0
- data/test/dummy/README.md +24 -0
- data/test/dummy/Rakefile +6 -0
- data/test/dummy/app/assets/stylesheets/application.css +10 -0
- data/test/dummy/app/controllers/application_controller.rb +4 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/models/application_record.rb +2 -0
- data/test/dummy/app/views/layouts/application.html.erb +28 -0
- data/test/dummy/app/views/pwa/manifest.json.erb +22 -0
- data/test/dummy/app/views/pwa/service-worker.js +26 -0
- data/test/dummy/bin/ci +6 -0
- data/test/dummy/bin/dev +2 -0
- data/test/dummy/bin/rails +4 -0
- data/test/dummy/bin/rake +4 -0
- data/test/dummy/bin/setup +35 -0
- data/test/dummy/config/application.rb +43 -0
- data/test/dummy/config/boot.rb +3 -0
- data/test/dummy/config/ci.rb +19 -0
- data/test/dummy/config/database.yml +31 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +57 -0
- data/test/dummy/config/environments/production.rb +67 -0
- data/test/dummy/config/environments/test.rb +42 -0
- data/test/dummy/config/initializers/assets.rb +7 -0
- data/test/dummy/config/initializers/content_security_policy.rb +29 -0
- data/test/dummy/config/initializers/filter_parameter_logging.rb +8 -0
- data/test/dummy/config/initializers/inflections.rb +16 -0
- data/test/dummy/config/locales/en.yml +31 -0
- data/test/dummy/config/puma.rb +39 -0
- data/test/dummy/config/routes.rb +16 -0
- data/test/dummy/config.ru +6 -0
- data/test/dummy/db/seeds.rb +9 -0
- data/test/dummy/log/test.log +441 -0
- data/test/dummy/public/400.html +135 -0
- data/test/dummy/public/404.html +135 -0
- data/test/dummy/public/406-unsupported-browser.html +135 -0
- data/test/dummy/public/422.html +135 -0
- data/test/dummy/public/500.html +135 -0
- data/test/dummy/public/icon.png +0 -0
- data/test/dummy/public/icon.svg +3 -0
- data/test/dummy/public/robots.txt +1 -0
- data/test/dummy/test/test_helper.rb +15 -0
- data/test/dummy/tmp/local_secret.txt +1 -0
- data/test/fixtures/docs/progress/PROGRESS_REPORT.md +6 -0
- data/test/integration/dashboard_test.rb +13 -0
- data/test/integration/docs_test.rb +46 -0
- data/test/integration/theme_test.rb +27 -0
- data/test/lib/markdown_renderer_test.rb +20 -0
- data/test/lib/theme_palette_test.rb +24 -0
- data/test/test_helper.rb +11 -0
- metadata +264 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
<% content_for :title, "#{resource_config.human_name_plural} - Admin Suite" %>
|
|
2
|
+
|
|
3
|
+
<%
|
|
4
|
+
has_new_route = begin
|
|
5
|
+
url_for(action: :new)
|
|
6
|
+
true
|
|
7
|
+
rescue ActionController::UrlGenerationError
|
|
8
|
+
false
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
has_edit_route = ->(record) {
|
|
12
|
+
begin
|
|
13
|
+
url_for(action: :edit, id: record.to_param)
|
|
14
|
+
true
|
|
15
|
+
rescue ActionController::UrlGenerationError
|
|
16
|
+
false
|
|
17
|
+
end
|
|
18
|
+
}
|
|
19
|
+
%>
|
|
20
|
+
|
|
21
|
+
<div class="px-4 sm:px-6 lg:px-8 max-w-7xl mx-auto">
|
|
22
|
+
<!-- Header -->
|
|
23
|
+
<div class="mb-6">
|
|
24
|
+
<div class="flex items-center justify-between">
|
|
25
|
+
<div>
|
|
26
|
+
<nav class="flex items-center gap-2 text-sm text-slate-500 mb-2">
|
|
27
|
+
<%= link_to "Dashboard", root_path, class: theme_link_hover_text_class %>
|
|
28
|
+
<%= admin_suite_icon("chevron-right", class: "w-4 h-4") %>
|
|
29
|
+
<span class="text-slate-900"><%= resource_config.human_name_plural %></span>
|
|
30
|
+
</nav>
|
|
31
|
+
<h1 class="text-2xl font-bold text-slate-900"><%= resource_config.human_name_plural %></h1>
|
|
32
|
+
</div>
|
|
33
|
+
<% if has_new_route %>
|
|
34
|
+
<div class="flex items-center gap-3">
|
|
35
|
+
<%= link_to url_for(action: :new), class: theme_btn_primary_class do %>
|
|
36
|
+
<%= admin_suite_icon("plus", class: "w-4 h-4") %>
|
|
37
|
+
New <%= resource_config.human_name %>
|
|
38
|
+
<% end %>
|
|
39
|
+
</div>
|
|
40
|
+
<% end %>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<!-- Stats (refresh with Turbo when filters change) -->
|
|
45
|
+
<%= turbo_frame_tag "resource_stats" do %>
|
|
46
|
+
<% if @stats.present? %>
|
|
47
|
+
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
|
48
|
+
<% @stats.each do |stat| %>
|
|
49
|
+
<% color = stat[:color] || :slate %>
|
|
50
|
+
<div class="bg-white rounded-xl p-4 border border-slate-200">
|
|
51
|
+
<div class="text-2xl font-bold text-<%= color %>-600"><%= stat[:value] %></div>
|
|
52
|
+
<div class="text-sm text-slate-500"><%= stat[:name] %></div>
|
|
53
|
+
</div>
|
|
54
|
+
<% end %>
|
|
55
|
+
</div>
|
|
56
|
+
<% end %>
|
|
57
|
+
<% end %>
|
|
58
|
+
|
|
59
|
+
<div class="flex flex-col lg:flex-row gap-6">
|
|
60
|
+
<!-- Filters Sidebar -->
|
|
61
|
+
<% if resource_config.index_config&.filters_list&.any? || resource_config.index_config&.searchable_fields&.any? %>
|
|
62
|
+
<div class="lg:w-64 flex-shrink-0">
|
|
63
|
+
<div class="bg-white rounded-xl border border-slate-200 p-4">
|
|
64
|
+
<h3 class="font-medium text-slate-900 dark:text-white mb-4">Filters</h3>
|
|
65
|
+
<%= form_with url: url_for(action: :index), method: :get,
|
|
66
|
+
data: {
|
|
67
|
+
turbo_frame: "resource_results",
|
|
68
|
+
controller: "admin-suite--live-filter",
|
|
69
|
+
action: "input->admin-suite--live-filter#submit change->admin-suite--live-filter#submit"
|
|
70
|
+
},
|
|
71
|
+
class: "space-y-4" do %>
|
|
72
|
+
|
|
73
|
+
<% if resource_config.index_config&.searchable_fields&.any? %>
|
|
74
|
+
<div>
|
|
75
|
+
<label class="block text-sm font-medium text-slate-700 mb-1">Search</label>
|
|
76
|
+
<%= text_field_tag :search, params[:search],
|
|
77
|
+
class: "form-input w-full",
|
|
78
|
+
placeholder: "Type 3+ characters to search...",
|
|
79
|
+
minlength: 3,
|
|
80
|
+
data: {
|
|
81
|
+
"admin-suite--live-filter-target": "input",
|
|
82
|
+
action: "input->admin-suite--live-filter#debounceWithMinLength",
|
|
83
|
+
"admin-suite--live-filter-min-length-value": 3
|
|
84
|
+
} %>
|
|
85
|
+
</div>
|
|
86
|
+
<% end %>
|
|
87
|
+
|
|
88
|
+
<% resource_config.index_config&.filters_list&.each do |filter| %>
|
|
89
|
+
<div>
|
|
90
|
+
<label class="block text-sm font-medium text-slate-700 mb-1">
|
|
91
|
+
<%= filter.label %>
|
|
92
|
+
</label>
|
|
93
|
+
<% case filter.type %>
|
|
94
|
+
<% when :text %>
|
|
95
|
+
<%= text_field_tag filter.name, params[filter.name],
|
|
96
|
+
class: "form-input w-full",
|
|
97
|
+
placeholder: filter.placeholder,
|
|
98
|
+
data: { "admin-suite--live-filter-target": "input", action: "input->admin-suite--live-filter#debounce" } %>
|
|
99
|
+
<% when :select %>
|
|
100
|
+
<% options = filter.options.is_a?(Proc) ? filter.options.call : filter.options %>
|
|
101
|
+
<%= select_tag filter.name,
|
|
102
|
+
options_for_select(options, params[filter.name]),
|
|
103
|
+
class: "form-input w-full",
|
|
104
|
+
data: { "admin-suite--live-filter-target": "input", action: "change->admin-suite--live-filter#submit" } %>
|
|
105
|
+
<% when :date %>
|
|
106
|
+
<%= date_field_tag filter.name, params[filter.name],
|
|
107
|
+
class: "form-input w-full",
|
|
108
|
+
data: { "admin-suite--live-filter-target": "input", action: "change->admin-suite--live-filter#submit" } %>
|
|
109
|
+
<% when :number %>
|
|
110
|
+
<%= number_field_tag filter.name, params[filter.name],
|
|
111
|
+
class: "form-input w-full",
|
|
112
|
+
placeholder: filter.placeholder,
|
|
113
|
+
data: { "admin-suite--live-filter-target": "input", action: "input->admin-suite--live-filter#debounce" } %>
|
|
114
|
+
<% when :toggle %>
|
|
115
|
+
<div class="flex items-center">
|
|
116
|
+
<%= check_box_tag filter.name, "1", params[filter.name] == "1",
|
|
117
|
+
class: "form-checkbox",
|
|
118
|
+
data: { "admin-suite--live-filter-target": "input", action: "change->admin-suite--live-filter#submit" } %>
|
|
119
|
+
<span class="ml-2 text-sm text-slate-600"><%= filter.label %></span>
|
|
120
|
+
</div>
|
|
121
|
+
<% end %>
|
|
122
|
+
</div>
|
|
123
|
+
<% end %>
|
|
124
|
+
|
|
125
|
+
<div class="flex gap-2 pt-2">
|
|
126
|
+
<button type="submit" class="admin-suite-btn-primary flex-1 text-sm font-medium rounded-lg transition-colors">
|
|
127
|
+
Apply
|
|
128
|
+
</button>
|
|
129
|
+
<%= link_to "Clear", url_for(action: :index),
|
|
130
|
+
class: "px-3 py-2 text-sm font-medium text-slate-600 hover:bg-slate-100 rounded-lg transition-colors",
|
|
131
|
+
data: { turbo_frame: "resource_results" } %>
|
|
132
|
+
</div>
|
|
133
|
+
<% end %>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
<% end %>
|
|
137
|
+
|
|
138
|
+
<!-- Main Content -->
|
|
139
|
+
<div class="flex-1">
|
|
140
|
+
<%= turbo_frame_tag "resource_results", data: { turbo_action: "advance" } do %>
|
|
141
|
+
<div class="bg-white rounded-xl border border-slate-200 overflow-hidden">
|
|
142
|
+
<% if @collection.any? %>
|
|
143
|
+
<div class="overflow-x-auto">
|
|
144
|
+
<table class="min-w-full divide-y divide-slate-200">
|
|
145
|
+
<thead class="bg-slate-50">
|
|
146
|
+
<tr>
|
|
147
|
+
<%
|
|
148
|
+
current_sort = params[:sort]&.to_sym || resource_config.index_config.default_sort
|
|
149
|
+
default_direction = resource_config.index_config.default_sort_direction&.to_s || "desc"
|
|
150
|
+
current_direction = params[:direction] || (current_sort == resource_config.index_config.default_sort ? default_direction : "asc")
|
|
151
|
+
%>
|
|
152
|
+
<% resource_config.index_config.columns_list.each do |column| %>
|
|
153
|
+
<% is_sortable = column.sortable %>
|
|
154
|
+
<% is_current_sort = current_sort == column.name %>
|
|
155
|
+
<% next_direction = is_current_sort && current_direction == "asc" ? "desc" : "asc" %>
|
|
156
|
+
|
|
157
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">
|
|
158
|
+
<% if is_sortable %>
|
|
159
|
+
<%= link_to url_for(params.permit!.merge(sort: column.name, direction: next_direction)),
|
|
160
|
+
class: "inline-flex items-center gap-1 hover:text-slate-900 transition-colors group",
|
|
161
|
+
data: { turbo_frame: "resource_results" } do %>
|
|
162
|
+
<span class="<%= is_current_sort ? 'text-indigo-600' : '' %>"><%= column.header %></span>
|
|
163
|
+
<% if is_current_sort %>
|
|
164
|
+
<svg class="w-3.5 h-3.5 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
165
|
+
<% if current_direction == "asc" %>
|
|
166
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7"/>
|
|
167
|
+
<% else %>
|
|
168
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
|
169
|
+
<% end %>
|
|
170
|
+
</svg>
|
|
171
|
+
<% else %>
|
|
172
|
+
<svg class="w-3.5 h-3.5 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
173
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4"/>
|
|
174
|
+
</svg>
|
|
175
|
+
<% end %>
|
|
176
|
+
<% end %>
|
|
177
|
+
<% else %>
|
|
178
|
+
<%= column.header %>
|
|
179
|
+
<% end %>
|
|
180
|
+
</th>
|
|
181
|
+
<% end %>
|
|
182
|
+
<th class="px-4 py-3 text-right text-xs font-medium text-slate-500 uppercase tracking-wider">
|
|
183
|
+
Actions
|
|
184
|
+
</th>
|
|
185
|
+
</tr>
|
|
186
|
+
</thead>
|
|
187
|
+
<tbody class="divide-y divide-slate-200">
|
|
188
|
+
<% @collection.each do |record| %>
|
|
189
|
+
<tr class="hover:bg-slate-50">
|
|
190
|
+
<% resource_config.index_config.columns_list.each do |column| %>
|
|
191
|
+
<td class="px-4 py-3 text-sm text-slate-900">
|
|
192
|
+
<%= render_column_value(record, column) %>
|
|
193
|
+
</td>
|
|
194
|
+
<% end %>
|
|
195
|
+
<td class="px-4 py-3 text-right text-sm">
|
|
196
|
+
<div class="flex items-center justify-end gap-2">
|
|
197
|
+
<%= link_to url_for(action: :show, id: record.to_param),
|
|
198
|
+
class: "inline-flex items-center px-2 py-1 rounded-md text-indigo-700 hover:text-indigo-800 hover:bg-indigo-50 transition-colors",
|
|
199
|
+
data: { turbo_frame: "_top" } do %>
|
|
200
|
+
View
|
|
201
|
+
<% end %>
|
|
202
|
+
<% if has_edit_route.call(record) %>
|
|
203
|
+
<%= link_to url_for(action: :edit, id: record.to_param),
|
|
204
|
+
class: "inline-flex items-center px-2 py-1 rounded-md text-slate-700 hover:text-slate-900 hover:bg-slate-100 transition-colors",
|
|
205
|
+
data: { turbo_frame: "_top" } do %>
|
|
206
|
+
Edit
|
|
207
|
+
<% end %>
|
|
208
|
+
<% end %>
|
|
209
|
+
</div>
|
|
210
|
+
</td>
|
|
211
|
+
</tr>
|
|
212
|
+
<% end %>
|
|
213
|
+
</tbody>
|
|
214
|
+
</table>
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
<!-- Pagination -->
|
|
218
|
+
<% if @pagy.pages > 1 %>
|
|
219
|
+
<div class="px-6 py-4 border-t border-slate-200 dark:border-slate-700">
|
|
220
|
+
<div class="flex items-center justify-between">
|
|
221
|
+
<div class="text-sm text-slate-500 dark:text-slate-400">
|
|
222
|
+
Showing <span class="font-medium text-slate-700 dark:text-slate-300"><%= @pagy.from %></span> to
|
|
223
|
+
<span class="font-medium text-slate-700 dark:text-slate-300"><%= @pagy.to %></span> of
|
|
224
|
+
<span class="font-medium text-slate-700 dark:text-slate-300"><%= number_with_delimiter(@pagy.count) %></span> results
|
|
225
|
+
</div>
|
|
226
|
+
|
|
227
|
+
<nav class="flex items-center gap-1">
|
|
228
|
+
<% if @pagy.prev %>
|
|
229
|
+
<%= link_to url_for(params.permit!.merge(page: @pagy.prev)),
|
|
230
|
+
class: "inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium text-slate-700 dark:text-slate-300 bg-white dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-600 transition-colors" do %>
|
|
231
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
232
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
|
233
|
+
</svg>
|
|
234
|
+
Prev
|
|
235
|
+
<% end %>
|
|
236
|
+
<% else %>
|
|
237
|
+
<span class="inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium text-slate-400 dark:text-slate-500 bg-slate-100 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg cursor-not-allowed">
|
|
238
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
239
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
|
240
|
+
</svg>
|
|
241
|
+
Prev
|
|
242
|
+
</span>
|
|
243
|
+
<% end %>
|
|
244
|
+
|
|
245
|
+
<div class="hidden sm:flex items-center gap-1 mx-2">
|
|
246
|
+
<% @pagy.series.each do |item| %>
|
|
247
|
+
<% if item == :gap %>
|
|
248
|
+
<span class="px-2 py-1 text-sm text-slate-400 dark:text-slate-500">…</span>
|
|
249
|
+
<% elsif item.is_a?(Integer) %>
|
|
250
|
+
<%= link_to item, url_for(params.permit!.merge(page: item)),
|
|
251
|
+
class: "px-3 py-1.5 text-sm font-medium text-slate-700 dark:text-slate-300 bg-white dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-600 transition-colors" %>
|
|
252
|
+
<% elsif item.is_a?(String) %>
|
|
253
|
+
<span class="px-3 py-1.5 text-sm font-medium text-white rounded-lg" style="background: var(--admin-suite-primary); border: 1px solid var(--admin-suite-primary);">
|
|
254
|
+
<%= item %>
|
|
255
|
+
</span>
|
|
256
|
+
<% end %>
|
|
257
|
+
<% end %>
|
|
258
|
+
</div>
|
|
259
|
+
|
|
260
|
+
<span class="sm:hidden px-3 py-1.5 text-sm text-slate-500 dark:text-slate-400">
|
|
261
|
+
Page <%= @pagy.page %> of <%= @pagy.pages %>
|
|
262
|
+
</span>
|
|
263
|
+
|
|
264
|
+
<% if @pagy.next %>
|
|
265
|
+
<%= link_to url_for(params.permit!.merge(page: @pagy.next)),
|
|
266
|
+
class: "inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium text-slate-700 dark:text-slate-300 bg-white dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-600 transition-colors" do %>
|
|
267
|
+
Next
|
|
268
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
269
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
|
270
|
+
</svg>
|
|
271
|
+
<% end %>
|
|
272
|
+
<% else %>
|
|
273
|
+
<span class="inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium text-slate-400 dark:text-slate-500 bg-slate-100 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg cursor-not-allowed">
|
|
274
|
+
Next
|
|
275
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
276
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
|
277
|
+
</svg>
|
|
278
|
+
</span>
|
|
279
|
+
<% end %>
|
|
280
|
+
</nav>
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
<% else %>
|
|
284
|
+
<div class="px-6 py-3 border-t border-slate-200 dark:border-slate-700">
|
|
285
|
+
<div class="text-sm text-slate-500 dark:text-slate-400">
|
|
286
|
+
Showing <span class="font-medium text-slate-700 dark:text-slate-300"><%= @pagy.count %></span> results
|
|
287
|
+
</div>
|
|
288
|
+
</div>
|
|
289
|
+
<% end %>
|
|
290
|
+
<% else %>
|
|
291
|
+
<div class="p-12 text-center">
|
|
292
|
+
<div class="inline-flex items-center justify-center w-12 h-12 rounded-full bg-slate-100 dark:bg-slate-900 mb-4">
|
|
293
|
+
<svg class="w-6 h-6 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
294
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"/>
|
|
295
|
+
</svg>
|
|
296
|
+
</div>
|
|
297
|
+
<h3 class="text-lg font-medium text-slate-900 dark:text-white">No <%= resource_config.human_name_plural.downcase %> found</h3>
|
|
298
|
+
<% if has_new_route %>
|
|
299
|
+
<p class="text-slate-500 dark:text-slate-400 mt-1">Get started by creating your first one.</p>
|
|
300
|
+
<%= link_to url_for(action: :new),
|
|
301
|
+
class: theme_btn_primary_class + " mt-4",
|
|
302
|
+
data: { turbo_frame: "_top" } do %>
|
|
303
|
+
<%= admin_suite_icon("plus", class: "w-4 h-4") %>
|
|
304
|
+
New <%= resource_config.human_name %>
|
|
305
|
+
<% end %>
|
|
306
|
+
<% else %>
|
|
307
|
+
<p class="text-slate-500 dark:text-slate-400 mt-1">No records have been created yet.</p>
|
|
308
|
+
<% end %>
|
|
309
|
+
</div>
|
|
310
|
+
<% end %>
|
|
311
|
+
</div>
|
|
312
|
+
<% end %>
|
|
313
|
+
</div>
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<% content_for :title, "New #{resource_config.human_name} - Admin Suite" %>
|
|
2
|
+
|
|
3
|
+
<div class="px-4 sm:px-6 lg:px-8 max-w-3xl mx-auto">
|
|
4
|
+
<!-- Header -->
|
|
5
|
+
<div class="mb-6">
|
|
6
|
+
<nav class="flex items-center gap-2 text-sm text-slate-500 dark:text-slate-400 mb-2">
|
|
7
|
+
<%= link_to "Dashboard", root_path, class: theme_link_hover_text_class %>
|
|
8
|
+
<%= admin_suite_icon("chevron-right", class: "w-4 h-4") %>
|
|
9
|
+
<%= link_to resource_config.human_name_plural, url_for(action: :index), class: theme_link_hover_text_class %>
|
|
10
|
+
<%= admin_suite_icon("chevron-right", class: "w-4 h-4") %>
|
|
11
|
+
<span class="text-slate-900 dark:text-white">New</span>
|
|
12
|
+
</nav>
|
|
13
|
+
<h1 class="text-2xl font-bold text-slate-900 dark:text-white">New <%= resource_config.human_name %></h1>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<!-- Form -->
|
|
17
|
+
<div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 overflow-hidden">
|
|
18
|
+
<div class="p-6">
|
|
19
|
+
<%= render "admin_suite/shared/form", resource: @resource %>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
<% content_for :title, "#{resource.respond_to?(:name) ? resource.name : "#{resource_config.human_name} ##{resource.id}"} - Admin Suite" %>
|
|
2
|
+
|
|
3
|
+
<%
|
|
4
|
+
has_edit_route = begin
|
|
5
|
+
url_for(action: :edit, id: resource.to_param)
|
|
6
|
+
true
|
|
7
|
+
rescue ActionController::UrlGenerationError
|
|
8
|
+
false
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
has_destroy_route = begin
|
|
12
|
+
url_for(action: :destroy, id: resource.to_param)
|
|
13
|
+
true
|
|
14
|
+
rescue ActionController::UrlGenerationError
|
|
15
|
+
false
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
sidebar_sections = resource_config.show_config&.sidebar_sections || []
|
|
19
|
+
main_sections = resource_config.show_config&.main_sections || []
|
|
20
|
+
|
|
21
|
+
if sidebar_sections.empty? && main_sections.empty? && resource_config.show_config&.sections_list&.any?
|
|
22
|
+
main_sections = resource_config.show_config.sections_list
|
|
23
|
+
end
|
|
24
|
+
%>
|
|
25
|
+
|
|
26
|
+
<div class="px-4 sm:px-6 lg:px-8 max-w-7xl mx-auto">
|
|
27
|
+
<!-- Header -->
|
|
28
|
+
<div class="mb-6">
|
|
29
|
+
<nav class="flex items-center gap-2 text-sm text-slate-500 dark:text-slate-400 mb-2">
|
|
30
|
+
<%= link_to "Dashboard", root_path, class: theme_link_hover_text_class %>
|
|
31
|
+
<%= admin_suite_icon("chevron-right", class: "w-4 h-4") %>
|
|
32
|
+
<%= link_to resource_config.human_name_plural, url_for(action: :index), class: theme_link_hover_text_class %>
|
|
33
|
+
<%= admin_suite_icon("chevron-right", class: "w-4 h-4") %>
|
|
34
|
+
<span class="text-slate-900 dark:text-white">
|
|
35
|
+
<%= resource.respond_to?(:name) ? resource.name.to_s.truncate(30) : "##{resource.id}" %>
|
|
36
|
+
</span>
|
|
37
|
+
</nav>
|
|
38
|
+
|
|
39
|
+
<div class="flex items-center justify-between">
|
|
40
|
+
<h1 class="text-2xl font-bold text-slate-900 dark:text-white">
|
|
41
|
+
<%= resource.respond_to?(:name) ? resource.name : "#{resource_config.human_name} ##{resource.id}" %>
|
|
42
|
+
</h1>
|
|
43
|
+
<div class="flex items-center gap-2">
|
|
44
|
+
<% if has_edit_route %>
|
|
45
|
+
<%= link_to url_for(action: :edit, id: resource.to_param), class: "inline-flex items-center gap-2 px-4 py-2 bg-slate-100 hover:bg-slate-200 dark:bg-slate-700 dark:hover:bg-slate-600 text-slate-900 dark:text-white text-sm font-medium rounded-lg transition-colors" do %>
|
|
46
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
47
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
|
|
48
|
+
</svg>
|
|
49
|
+
Edit
|
|
50
|
+
<% end %>
|
|
51
|
+
<% end %>
|
|
52
|
+
<% if has_destroy_route %>
|
|
53
|
+
<%= button_to url_for(action: :destroy, id: resource.to_param),
|
|
54
|
+
method: :delete,
|
|
55
|
+
form: { data: { turbo_confirm: "Delete this #{resource_config.human_name.downcase}? This cannot be undone.", turbo_frame: "_top" } },
|
|
56
|
+
class: "inline-flex items-center gap-2 px-4 py-2 bg-red-600 hover:bg-red-700 text-white text-sm font-medium rounded-lg transition-colors" do %>
|
|
57
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
58
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
|
59
|
+
</svg>
|
|
60
|
+
Delete
|
|
61
|
+
<% end %>
|
|
62
|
+
<% end %>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<!-- Resource Actions -->
|
|
68
|
+
<% if resource_config.actions_config&.member_actions&.any? %>
|
|
69
|
+
<div class="mb-6 flex flex-wrap gap-2">
|
|
70
|
+
<% resource_config.actions_config.member_actions.each do |action_def| %>
|
|
71
|
+
<%
|
|
72
|
+
show_action = true
|
|
73
|
+
if action_def.if_condition.present?
|
|
74
|
+
show_action =
|
|
75
|
+
if action_def.if_condition.arity.zero?
|
|
76
|
+
resource.instance_exec(&action_def.if_condition)
|
|
77
|
+
else
|
|
78
|
+
action_def.if_condition.call(resource)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
if action_def.unless_condition.present? && show_action
|
|
82
|
+
show_action =
|
|
83
|
+
if action_def.unless_condition.arity.zero?
|
|
84
|
+
!resource.instance_exec(&action_def.unless_condition)
|
|
85
|
+
else
|
|
86
|
+
!action_def.unless_condition.call(resource)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
controller_has_action = controller.action_methods.include?(action_def.name.to_s)
|
|
91
|
+
|
|
92
|
+
action_url =
|
|
93
|
+
if controller_has_action
|
|
94
|
+
begin
|
|
95
|
+
url_for(action: action_def.name, id: resource.to_param)
|
|
96
|
+
rescue ActionController::UrlGenerationError
|
|
97
|
+
nil
|
|
98
|
+
end
|
|
99
|
+
elsif action_def.type == :button
|
|
100
|
+
execute_action_path(
|
|
101
|
+
portal: current_portal,
|
|
102
|
+
resource_name: resource_name,
|
|
103
|
+
id: resource.to_param,
|
|
104
|
+
action_name: action_def.name
|
|
105
|
+
)
|
|
106
|
+
end
|
|
107
|
+
%>
|
|
108
|
+
<% if show_action && action_url %>
|
|
109
|
+
<% button_class = action_def.color == :danger ? "inline-flex items-center gap-2 px-4 py-2 bg-red-600 hover:bg-red-700 text-white text-sm font-medium rounded-lg transition-colors" : "admin-suite-btn-primary text-sm font-medium rounded-lg" %>
|
|
110
|
+
|
|
111
|
+
<% if action_def.type == :modal || action_def.type == :link %>
|
|
112
|
+
<%= link_to action_url, class: button_class, data: { turbo_frame: "_top" } do %>
|
|
113
|
+
<%= action_def.label %>
|
|
114
|
+
<% end %>
|
|
115
|
+
<% else %>
|
|
116
|
+
<% form_data = { turbo_frame: "_top" } %>
|
|
117
|
+
<% form_data[:turbo_confirm] = action_def.confirm if action_def.confirm.present? %>
|
|
118
|
+
<%= button_to action_url,
|
|
119
|
+
method: action_def.method || :post,
|
|
120
|
+
form: { data: form_data },
|
|
121
|
+
class: button_class do %>
|
|
122
|
+
<%= action_def.label %>
|
|
123
|
+
<% end %>
|
|
124
|
+
<% end %>
|
|
125
|
+
<% end %>
|
|
126
|
+
<% end %>
|
|
127
|
+
</div>
|
|
128
|
+
<% end %>
|
|
129
|
+
|
|
130
|
+
<!-- Two Column Layout -->
|
|
131
|
+
<div class="flex flex-col lg:flex-row gap-6">
|
|
132
|
+
<!-- Sidebar (Left Column) -->
|
|
133
|
+
<% if sidebar_sections.any? %>
|
|
134
|
+
<div class="lg:w-80 flex-shrink-0 space-y-6">
|
|
135
|
+
<% sidebar_sections.each do |section| %>
|
|
136
|
+
<%= render_show_section(resource, section, :sidebar) %>
|
|
137
|
+
<% end %>
|
|
138
|
+
</div>
|
|
139
|
+
<% else %>
|
|
140
|
+
<div class="lg:w-80 flex-shrink-0 space-y-6">
|
|
141
|
+
<div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 overflow-hidden">
|
|
142
|
+
<div class="px-4 py-3 border-b border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-900/50">
|
|
143
|
+
<h3 class="font-medium text-slate-900 dark:text-white text-sm">Details</h3>
|
|
144
|
+
</div>
|
|
145
|
+
<div class="p-4 space-y-3">
|
|
146
|
+
<div class="flex justify-between items-start">
|
|
147
|
+
<span class="text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider">ID</span>
|
|
148
|
+
<span class="text-sm font-mono text-slate-900 dark:text-white"><%= resource.id %></span>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
<% end %>
|
|
154
|
+
|
|
155
|
+
<!-- Main Content (Right Column) -->
|
|
156
|
+
<div class="flex-1 space-y-6 min-w-0">
|
|
157
|
+
<% if main_sections.any? %>
|
|
158
|
+
<% main_sections.each do |section| %>
|
|
159
|
+
<%= render_show_section(resource, section, :main) %>
|
|
160
|
+
<% end %>
|
|
161
|
+
<% else %>
|
|
162
|
+
<div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 overflow-hidden">
|
|
163
|
+
<div class="px-6 py-4 border-b border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-900/50">
|
|
164
|
+
<h3 class="font-medium text-slate-900 dark:text-white">All Attributes</h3>
|
|
165
|
+
</div>
|
|
166
|
+
<div class="p-6">
|
|
167
|
+
<dl class="space-y-6">
|
|
168
|
+
<% resource.attributes.except("id", "created_at", "updated_at").each do |attr, _value| %>
|
|
169
|
+
<div>
|
|
170
|
+
<dt class="text-sm font-medium text-slate-500 dark:text-slate-400 mb-2">
|
|
171
|
+
<%= attr.humanize %>
|
|
172
|
+
</dt>
|
|
173
|
+
<dd class="text-sm text-slate-900 dark:text-white">
|
|
174
|
+
<%= format_show_value(resource, attr) %>
|
|
175
|
+
</dd>
|
|
176
|
+
</div>
|
|
177
|
+
<% end %>
|
|
178
|
+
</dl>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
<% end %>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<% if flash.any? %>
|
|
2
|
+
<div class="fixed top-4 right-4 z-50 space-y-2 max-w-md">
|
|
3
|
+
<% flash.each do |type, message| %>
|
|
4
|
+
<% type = type.to_sym %>
|
|
5
|
+
<% classes = case type
|
|
6
|
+
when :notice
|
|
7
|
+
"bg-green-50 border-green-200 text-green-800"
|
|
8
|
+
when :alert
|
|
9
|
+
"bg-red-50 border-red-200 text-red-800"
|
|
10
|
+
when :warning
|
|
11
|
+
"bg-amber-50 border-amber-200 text-amber-800"
|
|
12
|
+
else
|
|
13
|
+
"admin-suite-flash--info"
|
|
14
|
+
end %>
|
|
15
|
+
|
|
16
|
+
<div class="flex items-start gap-3 px-5 py-4 rounded-lg shadow-lg border <%= classes %>">
|
|
17
|
+
<div class="flex-shrink-0 mt-0.5">
|
|
18
|
+
<% icon_name = case type
|
|
19
|
+
when :notice then "check-circle-2"
|
|
20
|
+
when :alert then "alert-triangle"
|
|
21
|
+
when :warning then "triangle-alert"
|
|
22
|
+
else "info"
|
|
23
|
+
end %>
|
|
24
|
+
<%= admin_suite_icon(icon_name, class: "w-5 h-5") %>
|
|
25
|
+
</div>
|
|
26
|
+
<p class="flex-1 text-sm font-medium leading-snug"><%= message %></p>
|
|
27
|
+
</div>
|
|
28
|
+
<% end %>
|
|
29
|
+
</div>
|
|
30
|
+
<% end %>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<%# Generic form for resources based on form_config %>
|
|
2
|
+
|
|
3
|
+
<%= form_with model: resource, url: resource.new_record? ? url_for(action: :create) : url_for(action: :update, id: resource.to_param), local: true, class: "space-y-6" do |f| %>
|
|
4
|
+
<% if resource.errors.any? %>
|
|
5
|
+
<div class="p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
|
|
6
|
+
<div class="flex items-start gap-3">
|
|
7
|
+
<svg class="w-5 h-5 text-red-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
8
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
9
|
+
</svg>
|
|
10
|
+
<div>
|
|
11
|
+
<h3 class="text-sm font-medium text-red-800 dark:text-red-200">
|
|
12
|
+
<%= pluralize(resource.errors.count, "error") %> prohibited this record from being saved:
|
|
13
|
+
</h3>
|
|
14
|
+
<ul class="mt-2 text-sm text-red-700 dark:text-red-300 list-disc list-inside">
|
|
15
|
+
<% resource.errors.full_messages.each do |message| %>
|
|
16
|
+
<li><%= message %></li>
|
|
17
|
+
<% end %>
|
|
18
|
+
</ul>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
<% end %>
|
|
23
|
+
|
|
24
|
+
<% resource_config.form_config&.fields_list&.each do |field| %>
|
|
25
|
+
<% case field %>
|
|
26
|
+
<% when Admin::Base::Resource::SectionDefinition %>
|
|
27
|
+
<div class="border-t border-slate-200 dark:border-slate-700 pt-6 mt-6">
|
|
28
|
+
<h3 class="text-lg font-medium text-slate-900 dark:text-white mb-1"><%= field.title %></h3>
|
|
29
|
+
<% if field.description.present? %>
|
|
30
|
+
<p class="text-sm text-slate-500 dark:text-slate-400 mb-4"><%= field.description %></p>
|
|
31
|
+
<% end %>
|
|
32
|
+
</div>
|
|
33
|
+
<% when Admin::Base::Resource::SectionEnd %>
|
|
34
|
+
<%# Section end marker %>
|
|
35
|
+
<% when Admin::Base::Resource::RowDefinition %>
|
|
36
|
+
<%# Row layout markers are handled by helpers for now %>
|
|
37
|
+
<% when Admin::Base::Resource::RowEnd %>
|
|
38
|
+
<%# Row end marker %>
|
|
39
|
+
<% when Admin::Base::Resource::FieldDefinition %>
|
|
40
|
+
<%= render_form_field(f, field, resource) %>
|
|
41
|
+
<% end %>
|
|
42
|
+
<% end %>
|
|
43
|
+
|
|
44
|
+
<% if resource_config.form_config&.fields_list.blank? %>
|
|
45
|
+
<!-- Default fields if no form config -->
|
|
46
|
+
<% resource.class.column_names.reject { |c| %w[id created_at updated_at].include?(c) }.each do |column| %>
|
|
47
|
+
<div class="form-group">
|
|
48
|
+
<%= f.label column, class: "form-label" %>
|
|
49
|
+
<%= f.text_field column, class: "form-input w-full" %>
|
|
50
|
+
</div>
|
|
51
|
+
<% end %>
|
|
52
|
+
<% end %>
|
|
53
|
+
|
|
54
|
+
<div class="flex items-center justify-end gap-3 pt-6 border-t border-slate-200 dark:border-slate-700">
|
|
55
|
+
<%= link_to "Cancel", resource.new_record? ? url_for(action: :index) : url_for(action: :show, id: resource.to_param), class: "px-4 py-2 text-sm font-medium text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700 rounded-lg transition-colors" %>
|
|
56
|
+
<%= f.button class: theme_btn_primary_small_class, data: { disable_with: "Saving..." } do %>
|
|
57
|
+
<%= resource.new_record? ? "Create #{resource_config.human_name}" : "Save Changes" %>
|
|
58
|
+
<% end %>
|
|
59
|
+
</div>
|
|
60
|
+
<% end %>
|