elaine_crud 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.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/LICENSE +21 -0
  4. data/README.md +225 -0
  5. data/Rakefile +9 -0
  6. data/TODO.md +496 -0
  7. data/app/controllers/elaine_crud/base_controller.rb +228 -0
  8. data/app/helpers/elaine_crud/base_helper.rb +787 -0
  9. data/app/helpers/elaine_crud/search_helper.rb +132 -0
  10. data/app/javascript/controllers/dropdown_controller.js +18 -0
  11. data/app/views/elaine_crud/base/_edit_row.html.erb +60 -0
  12. data/app/views/elaine_crud/base/_export_button.html.erb +88 -0
  13. data/app/views/elaine_crud/base/_foreign_key_select_refresh.html.erb +52 -0
  14. data/app/views/elaine_crud/base/_form.html.erb +45 -0
  15. data/app/views/elaine_crud/base/_form_fields.html.erb +45 -0
  16. data/app/views/elaine_crud/base/_index_table.html.erb +58 -0
  17. data/app/views/elaine_crud/base/_modal.html.erb +71 -0
  18. data/app/views/elaine_crud/base/_pagination.html.erb +110 -0
  19. data/app/views/elaine_crud/base/_per_page_selector.html.erb +30 -0
  20. data/app/views/elaine_crud/base/_search_bar.html.erb +75 -0
  21. data/app/views/elaine_crud/base/_show_details.html.erb +29 -0
  22. data/app/views/elaine_crud/base/_view_row.html.erb +96 -0
  23. data/app/views/elaine_crud/base/edit.html.erb +51 -0
  24. data/app/views/elaine_crud/base/index.html.erb +74 -0
  25. data/app/views/elaine_crud/base/new.html.erb +12 -0
  26. data/app/views/elaine_crud/base/new_modal.html.erb +37 -0
  27. data/app/views/elaine_crud/base/not_found.html.erb +49 -0
  28. data/app/views/elaine_crud/base/show.html.erb +32 -0
  29. data/docs/ARCHITECTURE.md +410 -0
  30. data/docs/CSS_GRID_LAYOUT.md +126 -0
  31. data/docs/DEMO.md +693 -0
  32. data/docs/DSL_EXAMPLES.md +313 -0
  33. data/docs/FOREIGN_KEY_EXAMPLE.rb +100 -0
  34. data/docs/FOREIGN_KEY_SUPPORT.md +197 -0
  35. data/docs/HAS_MANY_IMPLEMENTATION.md +154 -0
  36. data/docs/LAYOUT_EXAMPLES.md +301 -0
  37. data/docs/TROUBLESHOOTING.md +170 -0
  38. data/elaine_crud.gemspec +46 -0
  39. data/lib/elaine_crud/dsl_methods.rb +348 -0
  40. data/lib/elaine_crud/engine.rb +37 -0
  41. data/lib/elaine_crud/export_handling.rb +164 -0
  42. data/lib/elaine_crud/field_configuration.rb +422 -0
  43. data/lib/elaine_crud/field_configuration_methods.rb +152 -0
  44. data/lib/elaine_crud/layout_calculation.rb +55 -0
  45. data/lib/elaine_crud/parameter_handling.rb +48 -0
  46. data/lib/elaine_crud/record_fetching.rb +150 -0
  47. data/lib/elaine_crud/relationship_handling.rb +220 -0
  48. data/lib/elaine_crud/routing.rb +33 -0
  49. data/lib/elaine_crud/search_and_filtering.rb +285 -0
  50. data/lib/elaine_crud/sorting_concern.rb +65 -0
  51. data/lib/elaine_crud/version.rb +5 -0
  52. data/lib/elaine_crud.rb +25 -0
  53. data/lib/tasks/demo.rake +111 -0
  54. data/lib/tasks/spec.rake +26 -0
  55. metadata +264 -0
@@ -0,0 +1,110 @@
1
+ <%# Pagination controls for navigating between pages %>
2
+ <% if records.total_pages > 1 %>
3
+ <div class="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
4
+ <%# Info section: "Showing X to Y of Z results" %>
5
+ <div class="flex-1 flex justify-between sm:hidden">
6
+ <%= link_to_unless records.first_page?, "Previous", url_for(page: records.prev_page, per_page: params[:per_page]),
7
+ class: "relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50" %>
8
+ <%= link_to_unless records.last_page?, "Next", url_for(page: records.next_page, per_page: params[:per_page]),
9
+ class: "ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50" %>
10
+ </div>
11
+
12
+ <div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
13
+ <div>
14
+ <p class="text-sm text-gray-700">
15
+ Showing
16
+ <span class="font-medium"><%= records.offset_value + 1 %></span>
17
+ to
18
+ <span class="font-medium"><%= [records.offset_value + records.count, records.total_count].min %></span>
19
+ of
20
+ <span class="font-medium"><%= records.total_count %></span>
21
+ results
22
+ </p>
23
+ </div>
24
+
25
+ <div>
26
+ <nav class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination">
27
+ <%# Previous button %>
28
+ <% if records.first_page? %>
29
+ <span class="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-gray-100 text-sm font-medium text-gray-400 cursor-not-allowed">
30
+ <span class="sr-only">Previous</span>
31
+ <svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
32
+ <path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd" />
33
+ </svg>
34
+ </span>
35
+ <% else %>
36
+ <%= link_to url_for(page: records.prev_page, per_page: params[:per_page]),
37
+ class: "relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50" do %>
38
+ <span class="sr-only">Previous</span>
39
+ <svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
40
+ <path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd" />
41
+ </svg>
42
+ <% end %>
43
+ <% end %>
44
+
45
+ <%# Page numbers with ellipsis %>
46
+ <%
47
+ current_page = records.current_page
48
+ total_pages = records.total_pages
49
+ window = 2 # Show 2 pages on each side of current page
50
+
51
+ # Always show first page
52
+ pages_to_show = [1]
53
+
54
+ # Add pages around current page
55
+ ((current_page - window)..(current_page + window)).each do |page|
56
+ pages_to_show << page if page > 1 && page < total_pages
57
+ end
58
+
59
+ # Always show last page
60
+ pages_to_show << total_pages if total_pages > 1
61
+
62
+ pages_to_show = pages_to_show.uniq.sort
63
+ %>
64
+
65
+ <%
66
+ previous_page = 0
67
+ pages_to_show.each do |page_num|
68
+ %>
69
+ <%# Show ellipsis if there's a gap %>
70
+ <% if page_num - previous_page > 1 %>
71
+ <span class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700">
72
+ ...
73
+ </span>
74
+ <% end %>
75
+
76
+ <%# Page number link or current page indicator %>
77
+ <% if page_num == current_page %>
78
+ <span class="z-10 bg-blue-50 border-blue-500 text-blue-600 relative inline-flex items-center px-4 py-2 border text-sm font-medium">
79
+ <%= page_num %>
80
+ </span>
81
+ <% else %>
82
+ <%= link_to page_num, url_for(page: page_num, per_page: params[:per_page]),
83
+ class: "bg-white border-gray-300 text-gray-500 hover:bg-gray-50 relative inline-flex items-center px-4 py-2 border text-sm font-medium" %>
84
+ <% end %>
85
+
86
+ <% previous_page = page_num %>
87
+ <% end %>
88
+
89
+ <%# Next button %>
90
+ <% if records.last_page? %>
91
+ <span class="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-gray-100 text-sm font-medium text-gray-400 cursor-not-allowed">
92
+ <span class="sr-only">Next</span>
93
+ <svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
94
+ <path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
95
+ </svg>
96
+ </span>
97
+ <% else %>
98
+ <%= link_to url_for(page: records.next_page, per_page: params[:per_page]),
99
+ class: "relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50" do %>
100
+ <span class="sr-only">Next</span>
101
+ <svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
102
+ <path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
103
+ </svg>
104
+ <% end %>
105
+ <% end %>
106
+ </nav>
107
+ </div>
108
+ </div>
109
+ </div>
110
+ <% end %>
@@ -0,0 +1,30 @@
1
+ <%# Per-page selector dropdown %>
2
+ <div class="flex items-center space-x-2">
3
+ <label for="per_page_select" class="text-sm text-gray-700">Show:</label>
4
+ <select id="per_page_select"
5
+ name="per_page"
6
+ class="block w-20 pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md"
7
+ onchange="this.form.submit()">
8
+ <% [10, 25, 50, 100].each do |option| %>
9
+ <option value="<%= option %>" <%= 'selected' if per_page == option %>>
10
+ <%= option %>
11
+ </option>
12
+ <% end %>
13
+ </select>
14
+ <span class="text-sm text-gray-700">per page</span>
15
+ </div>
16
+
17
+ <script>
18
+ // Handle form submission via Turbo
19
+ document.addEventListener('DOMContentLoaded', function() {
20
+ const select = document.getElementById('per_page_select');
21
+ if (select) {
22
+ select.addEventListener('change', function() {
23
+ const currentUrl = new URL(window.location.href);
24
+ currentUrl.searchParams.set('per_page', this.value);
25
+ currentUrl.searchParams.delete('page'); // Reset to page 1 when changing per_page
26
+ window.location.href = currentUrl.toString();
27
+ });
28
+ }
29
+ });
30
+ </script>
@@ -0,0 +1,75 @@
1
+ <div class="mb-6 bg-white p-4 rounded-lg shadow">
2
+ <%= form_with url: url_for(action: :index), method: :get, local: true, class: "space-y-4" do |f| %>
3
+
4
+ <!-- Global Search -->
5
+ <div class="flex items-center space-x-2">
6
+ <div class="flex-1">
7
+ <%= text_field_tag :search,
8
+ @search_query,
9
+ placeholder: "Search #{@model_name.pluralize.downcase}...",
10
+ class: "block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500" %>
11
+ </div>
12
+ <%= button_tag type: 'submit', class: "bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" do %>
13
+ 🔍 Search
14
+ <% end %>
15
+ <% if search_active? %>
16
+ <%= link_to "Clear", url_for(action: :index), class: "bg-gray-200 hover:bg-gray-300 text-gray-800 font-bold py-2 px-4 rounded" %>
17
+ <% end %>
18
+ </div>
19
+
20
+ <!-- Advanced Filters (Collapsible) -->
21
+ <% if @filterable_columns.any? %>
22
+ <details class="border-t pt-4">
23
+ <summary class="cursor-pointer text-blue-600 hover:text-blue-800 font-medium">
24
+ Advanced Filters
25
+ </summary>
26
+
27
+ <div class="mt-4 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
28
+ <% @filterable_columns.each do |field_info| %>
29
+ <%= render_filter_field(field_info) %>
30
+ <% end %>
31
+ </div>
32
+
33
+ <div class="mt-4 flex space-x-2">
34
+ <%= button_tag "Apply Filters", type: 'submit', class: "bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded" %>
35
+ <%= link_to "Clear All Filters", url_for(action: :index, search: @search_query), class: "bg-gray-200 hover:bg-gray-300 text-gray-800 font-bold py-2 px-4 rounded" %>
36
+ </div>
37
+ </details>
38
+ <% end %>
39
+
40
+ <!-- Preserve pagination and sorting params -->
41
+ <%= hidden_field_tag :per_page, params[:per_page] if params[:per_page] %>
42
+ <%= hidden_field_tag :sort, params[:sort] if params[:sort] %>
43
+ <%= hidden_field_tag :direction, params[:direction] if params[:direction] %>
44
+
45
+ <!-- Preserve parent filtering params -->
46
+ <% detect_parent_filters.each do |foreign_key, parent_id| %>
47
+ <%= hidden_field_tag foreign_key, parent_id %>
48
+ <% end %>
49
+ <% end %>
50
+ </div>
51
+
52
+ <!-- Active Filters Display -->
53
+ <% if @active_filters.any? %>
54
+ <div class="mb-4 flex flex-wrap gap-2">
55
+ <span class="text-sm font-medium text-gray-700">Active filters:</span>
56
+ <% @active_filters.each do |field, value| %>
57
+ <% next if field.to_s.end_with?('_from', '_to') %>
58
+ <% next if value.blank? %>
59
+ <span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-800">
60
+ <%= field.to_s.humanize %>: <%= value %>
61
+ <%= link_to "✕", url_for(filter: @active_filters.except(field), search: @search_query), class: "ml-2 text-blue-600 hover:text-blue-800" %>
62
+ </span>
63
+ <% end %>
64
+ </div>
65
+ <% end %>
66
+
67
+ <!-- Results Count -->
68
+ <% if search_active? %>
69
+ <div class="mb-4 text-sm text-gray-600">
70
+ Showing <%= @records.total_count %> result<%= @records.total_count == 1 ? '' : 's' %>
71
+ <% if @total_count && @total_count != @records.total_count %>
72
+ (filtered from <%= @total_count %> total)
73
+ <% end %>
74
+ </div>
75
+ <% end %>
@@ -0,0 +1,29 @@
1
+ <%# Reusable field details display for show views %>
2
+ <%# Required locals: record, columns %>
3
+ <%# Optional locals: title (string, default: nil) %>
4
+
5
+ <% title = local_assigns.fetch(:title, nil) %>
6
+
7
+ <div class="bg-white shadow-md rounded-lg p-6">
8
+ <% if title %>
9
+ <h2 class="text-xl font-bold text-gray-900 mb-4"><%= title %></h2>
10
+ <% end %>
11
+
12
+ <div class="space-y-4">
13
+ <% columns.each do |column| %>
14
+ <div class="border-b border-gray-200 pb-4">
15
+ <dt class="text-sm font-medium text-gray-500 mb-1">
16
+ <%= field_title(column.to_sym) %>
17
+ </dt>
18
+ <dd class="text-sm text-gray-900">
19
+ <%= display_field_value(record, column.to_sym, context: :show) %>
20
+ </dd>
21
+ <% if field_description(column.to_sym).present? %>
22
+ <p class="text-xs text-gray-400 mt-1">
23
+ <%= field_description(column.to_sym) %>
24
+ </p>
25
+ <% end %>
26
+ </div>
27
+ <% end %>
28
+ </div>
29
+ </div>
@@ -0,0 +1,96 @@
1
+ <%# View mode row - renders cells directly as grid children (no wrapper needed for grid) %>
2
+ <%# Calculate layout for this record %>
3
+ <% record_layout = calculate_layout(record, columns.map(&:to_sym)) %>
4
+ <% has_multiple_rows = record_layout.length > 1 %>
5
+ <% is_last_record = defined?(is_last_record) ? is_last_record : false %>
6
+ <% show_actions = local_assigns.fetch(:show_actions, true) %>
7
+
8
+ <%# Wrap entire row in turbo-frame for row-level inline editing %>
9
+ <% if controller.respond_to?(:turbo_disabled?) && controller.turbo_disabled? %>
10
+ <%# Turbo disabled - render cells directly without frame %>
11
+ <% record_layout.each_with_index do |layout_row, row_index| %>
12
+ <% is_last_row = row_index == record_layout.length - 1 %>
13
+ <% layout_row.each do |column_config| %>
14
+ <% field_name = column_config[:field_name] %>
15
+ <% colspan = column_config[:colspan] || 1 %>
16
+ <% rowspan = column_config[:rowspan] || 1 %>
17
+ <% cell_border_bottom = (has_multiple_rows && !is_last_row) || !is_last_record ? 'border-b' : '' %>
18
+
19
+ <div class="px-3 py-3 text-sm text-gray-900 border-r border-gray-200 <%= cell_border_bottom %> break-words <%= row_bg_class %> hover:bg-blue-50 <%= 'col-span-' + colspan.to_s if colspan > 1 %> <%= 'row-span-' + rowspan.to_s if rowspan > 1 %>">
20
+ <%= display_field_value(record, field_name) %>
21
+ </div>
22
+ <% end %>
23
+
24
+ <%# Actions column (only on first row) %>
25
+ <% if show_actions && row_index == 0 %>
26
+ <% actions_border_bottom = has_multiple_rows ? '' : (is_last_record ? '' : 'border-b') %>
27
+ <div class="px-3 py-3 text-sm text-gray-500 border-gray-200 <%= row_bg_class %> hover:bg-blue-50 <%= actions_border_bottom %> <%= 'row-span-' + record_layout.length.to_s if record_layout.length > 1 %>">
28
+ <div class="flex space-x-2">
29
+ <% if controller.class.show_view_action_button %>
30
+ <%= link_to "View",
31
+ url_for(action: :show, id: record.id),
32
+ class: "text-blue-600 hover:text-blue-900",
33
+ data: { turbo: false } %>
34
+ <% end %>
35
+ <%= link_to "Edit",
36
+ url_for(action: :edit, id: record.id, page: params[:page], per_page: params[:per_page]),
37
+ class: "text-green-600 hover:text-green-900",
38
+ data: { turbo: false } %>
39
+ <%= button_to "Delete",
40
+ url_for(action: :destroy, id: record.id),
41
+ method: :delete,
42
+ form: {
43
+ class: "inline",
44
+ data: { turbo: false },
45
+ onsubmit: "return confirm('Are you sure?');"
46
+ },
47
+ class: "text-red-600 hover:text-red-900" %>
48
+ </div>
49
+ </div>
50
+ <% end %>
51
+ <% end %>
52
+ <% else %>
53
+ <%# Turbo enabled - wrap entire row in single turbo-frame %>
54
+ <turbo-frame id="record_<%= record.id %>" class="contents">
55
+ <% record_layout.each_with_index do |layout_row, row_index| %>
56
+ <% is_last_row = row_index == record_layout.length - 1 %>
57
+ <% layout_row.each do |column_config| %>
58
+ <% field_name = column_config[:field_name] %>
59
+ <% colspan = column_config[:colspan] || 1 %>
60
+ <% rowspan = column_config[:rowspan] || 1 %>
61
+ <% cell_border_bottom = (has_multiple_rows && !is_last_row) || !is_last_record ? 'border-b' : '' %>
62
+
63
+ <div class="px-3 py-3 text-sm text-gray-900 border-r border-gray-200 <%= cell_border_bottom %> break-words <%= row_bg_class %> hover:bg-blue-50 <%= 'col-span-' + colspan.to_s if colspan > 1 %> <%= 'row-span-' + rowspan.to_s if rowspan > 1 %>">
64
+ <%= display_field_value(record, field_name) %>
65
+ </div>
66
+ <% end %>
67
+
68
+ <%# Actions column (only on first row) %>
69
+ <% if show_actions && row_index == 0 %>
70
+ <% actions_border_bottom = has_multiple_rows ? '' : (is_last_record ? '' : 'border-b') %>
71
+ <div class="px-3 py-3 text-sm text-gray-500 border-gray-200 <%= row_bg_class %> hover:bg-blue-50 <%= actions_border_bottom %> <%= 'row-span-' + record_layout.length.to_s if record_layout.length > 1 %>">
72
+ <div class="flex space-x-2">
73
+ <% if controller.class.show_view_action_button %>
74
+ <%= link_to "View",
75
+ url_for(action: :show, id: record.id),
76
+ class: "text-blue-600 hover:text-blue-900",
77
+ data: { turbo_frame: "_top" } %>
78
+ <% end %>
79
+ <%= link_to "Edit",
80
+ url_for(action: :edit, id: record.id, page: params[:page], per_page: params[:per_page]),
81
+ class: "text-green-600 hover:text-green-900" %>
82
+ <%= button_to "Delete",
83
+ url_for(action: :destroy, id: record.id),
84
+ method: :delete,
85
+ form: {
86
+ class: "inline",
87
+ data: { turbo: false },
88
+ onsubmit: "return confirm('Are you sure?');"
89
+ },
90
+ class: "text-red-600 hover:text-red-900" %>
91
+ </div>
92
+ </div>
93
+ <% end %>
94
+ <% end %>
95
+ </turbo-frame>
96
+ <% end %>
@@ -0,0 +1,51 @@
1
+ <div class="w-full">
2
+ <div class="flex justify-between items-center mb-6">
3
+ <h1 class="text-3xl font-bold text-gray-900">Edit <%= @model_name %> (ID: <%= @record.id %>)</h1>
4
+ <%= link_to "Back to #{@model_name.pluralize}",
5
+ url_for(action: :index, page: params[:page], per_page: params[:per_page]),
6
+ class: "text-gray-600 hover:text-gray-900" %>
7
+ </div>
8
+
9
+ <% if @records.any? %>
10
+ <%# Calculate layout header to determine grid structure - same as index page %>
11
+ <% header_layout = calculate_layout_header(@columns.map(&:to_sym)) %>
12
+ <% grid_template = header_layout.map { |config| config[:width] }.join(' ') %>
13
+
14
+ <div class="overflow-x-auto">
15
+ <%# Main grid container - grid template defined once here for entire table %>
16
+ <div class="bg-white shadow-md border border-gray-300 inline-block min-w-full grid" style="grid-template-columns: <%= grid_template %>;">
17
+ <%# Header row - no grid definition, inherits from parent %>
18
+ <% header_layout[0..-2].each do |header_config| %>
19
+ <div class="px-3 py-3 text-xs font-medium text-gray-700 uppercase tracking-wider border-r border-b-2 border-gray-200 bg-gray-100">
20
+ <%= header_column_title(header_config) %>
21
+ </div>
22
+ <% end %>
23
+ <div class="px-3 py-3 text-xs font-medium text-gray-700 uppercase tracking-wider border-b-2 border-gray-200 bg-gray-100">
24
+ Actions
25
+ </div>
26
+
27
+ <%# Records - each cell is a direct grid child %>
28
+ <% @records.each_with_index do |record, index| %>
29
+ <% record_id = "record_#{record.id}" %>
30
+ <% row_bg_class = index.even? ? "bg-white" : "bg-gray-50" %>
31
+ <% is_last_record = index == @records.length - 1 %>
32
+ <% if record.id == @record.id %>
33
+ <%# Render edit row for the record being edited %>
34
+ <%= render 'elaine_crud/base/edit_row', record: @record, columns: @columns, row_bg_class: row_bg_class, record_id: record_id, is_last_record: is_last_record %>
35
+ <% else %>
36
+ <%# Render normal view row %>
37
+ <%= render 'elaine_crud/base/view_row', record: record, columns: @columns, row_bg_class: row_bg_class, record_id: record_id, is_last_record: is_last_record %>
38
+ <% end %>
39
+ <% end %>
40
+ </div>
41
+ </div>
42
+ <% else %>
43
+ <div class="bg-gray-50 border border-gray-200 rounded-lg p-8 text-center">
44
+ <h3 class="text-lg font-medium text-gray-900 mb-2">No <%= @model_name.pluralize.downcase %> found</h3>
45
+ <p class="text-gray-600 mb-4">Get started by creating your first <%= @model_name.downcase %>.</p>
46
+ <%= link_to "Create #{@model_name}",
47
+ url_for(action: :new),
48
+ class: "bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" %>
49
+ </div>
50
+ <% end %>
51
+ </div>
@@ -0,0 +1,74 @@
1
+ <div class="px-4 py-8">
2
+ <% if @parent_context %>
3
+ <div class="bg-blue-50 border-l-4 border-blue-400 p-4 mb-6">
4
+ <div class="flex">
5
+ <div class="flex-shrink-0">
6
+ <svg class="h-5 w-5 text-blue-400" viewBox="0 0 20 20" fill="currentColor">
7
+ <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
8
+ </svg>
9
+ </div>
10
+ <div class="ml-3">
11
+ <p class="text-sm text-blue-700">
12
+ <% if @parent_context[:error] %>
13
+ <strong>Error:</strong> <%= @parent_context[:error] %>
14
+ <% else %>
15
+ Showing <%= @model_name.pluralize.downcase %> for
16
+ <strong><%= @parent_context[:model_class].name %>:</strong>
17
+ <%= @parent_context[:record].public_send(controller.send(:determine_display_field_for_model, @parent_context[:model_class])) %>
18
+ <% end %>
19
+
20
+ <span class="ml-4">
21
+ <%= link_to "View All #{@model_name.pluralize}",
22
+ url_for(action: :index, @parent_context[:foreign_key] => nil),
23
+ class: "text-blue-600 underline hover:text-blue-800" %>
24
+ </span>
25
+ </p>
26
+ </div>
27
+ </div>
28
+ </div>
29
+ <% end %>
30
+
31
+ <div class="flex justify-between items-center mb-6">
32
+ <div class="flex items-center space-x-4">
33
+ <h1 class="text-3xl font-bold text-gray-900"><%= @model_name.pluralize %></h1>
34
+ <% if @records.respond_to?(:total_count) %>
35
+ <%= render 'elaine_crud/base/per_page_selector', per_page: determine_per_page %>
36
+ <% end %>
37
+ </div>
38
+ <div class="flex gap-2">
39
+ <%= link_to "New #{@model_name}",
40
+ url_for(action: :new),
41
+ class: "bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" %>
42
+ <%= render 'elaine_crud/base/export_button' %>
43
+ </div>
44
+ </div>
45
+
46
+ <%# Search and Filter Bar %>
47
+ <%= render 'elaine_crud/base/search_bar' %>
48
+
49
+ <% if @records.any? %>
50
+ <%= render partial: 'elaine_crud/base/index_table',
51
+ locals: { records: @records, columns: @columns } %>
52
+
53
+ <%# Pagination controls %>
54
+ <% if @records.respond_to?(:total_pages) %>
55
+ <%= render 'elaine_crud/base/pagination', records: @records %>
56
+ <% end %>
57
+ <% else %>
58
+ <div class="bg-gray-50 border border-gray-200 rounded-lg p-8 text-center">
59
+ <% if search_active? %>
60
+ <h3 class="text-lg font-medium text-gray-900 mb-2">No results found</h3>
61
+ <p class="text-gray-600 mb-4">Try adjusting your search or filters.</p>
62
+ <%= link_to "Clear search and filters",
63
+ url_for(action: :index),
64
+ class: "text-blue-600 hover:text-blue-800 underline" %>
65
+ <% else %>
66
+ <h3 class="text-lg font-medium text-gray-900 mb-2">No <%= @model_name.pluralize.downcase %> found</h3>
67
+ <p class="text-gray-600 mb-4">Get started by creating your first <%= @model_name.downcase %>.</p>
68
+ <%= link_to "Create #{@model_name}",
69
+ url_for(action: :new),
70
+ class: "bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" %>
71
+ <% end %>
72
+ </div>
73
+ <% end %>
74
+ </div>
@@ -0,0 +1,12 @@
1
+ <div class="container mx-auto px-4 py-8">
2
+ <div class="flex justify-between items-center mb-6">
3
+ <h1 class="text-3xl font-bold text-gray-900">New <%= @model_name %></h1>
4
+ <%= link_to "Back to #{@model_name.pluralize}",
5
+ url_for(action: :index),
6
+ class: "text-gray-600 hover:text-gray-900" %>
7
+ </div>
8
+
9
+ <div class="bg-white shadow-md rounded-lg p-6">
10
+ <%= render 'form', record: @record, submit_text: "Create #{@model_name}" %>
11
+ </div>
12
+ </div>
@@ -0,0 +1,37 @@
1
+ <%# Modal view for creating nested records %>
2
+ <%= turbo_frame_tag "modal_content" do %>
3
+ <div class="p-6">
4
+ <h2 class="text-2xl font-bold text-gray-900 mb-6">
5
+ New <%= @model_name %>
6
+ </h2>
7
+
8
+ <%= form_with(model: @record,
9
+ url: url_for(action: :create),
10
+ method: :post,
11
+ data: { nested_create: true }) do |form| %>
12
+
13
+ <%# Hidden fields to track modal mode %>
14
+ <%= hidden_field_tag :modal_mode, 'true' %>
15
+ <%= hidden_field_tag :return_field, @return_field %>
16
+ <%= hidden_field_tag :parent_model, params[:parent_model] %>
17
+
18
+ <%# Render form fields %>
19
+ <div class="space-y-4">
20
+ <%= render partial: 'elaine_crud/base/form_fields',
21
+ locals: { form: form, record: @record } %>
22
+ </div>
23
+
24
+ <%# Form actions %>
25
+ <div class="mt-6 flex items-center justify-end space-x-3">
26
+ <button type="button"
27
+ onclick="ElaineCrud.closeModal()"
28
+ class="bg-gray-200 hover:bg-gray-300 text-gray-800 font-bold py-2 px-4 rounded">
29
+ Cancel
30
+ </button>
31
+
32
+ <%= form.submit "Create #{@model_name}",
33
+ class: "bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" %>
34
+ </div>
35
+ <% end %>
36
+ </div>
37
+ <% end %>
@@ -0,0 +1,49 @@
1
+ <div class="container mx-auto px-4 py-8">
2
+ <div class="max-w-3xl">
3
+ <%# Header %>
4
+ <div class="mb-6">
5
+ <div class="flex items-baseline gap-4 mb-2">
6
+ <h1 class="text-4xl font-bold text-gray-900">404</h1>
7
+ <h2 class="text-2xl font-semibold text-gray-700"><%= @model_name %> Not Found</h2>
8
+ </div>
9
+ <div class="h-1 w-24 bg-blue-600 rounded-full"></div>
10
+ </div>
11
+
12
+ <%# Error Message %>
13
+ <div class="bg-gray-50 border-l-4 border-blue-500 p-4 mb-6">
14
+ <div class="flex items-start">
15
+ <svg class="h-5 w-5 text-blue-500 mt-0.5 mr-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
16
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
17
+ </svg>
18
+ <div>
19
+ <p class="text-gray-700 font-medium">
20
+ The <%= @model_name.downcase %> with ID <span class="font-mono bg-gray-200 px-2 py-1 rounded text-sm"><%= @resource_id %></span> could not be found.
21
+ </p>
22
+ <p class="text-gray-600 text-sm mt-1">
23
+ It may have been deleted or the ID might be incorrect.
24
+ </p>
25
+ </div>
26
+ </div>
27
+ </div>
28
+
29
+ <%# Helpful Actions %>
30
+ <div class="space-y-3">
31
+ <div class="flex gap-3">
32
+ <%= link_to "View All #{@model_name.pluralize}", polymorphic_path(@model_class),
33
+ class: "inline-flex items-center px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded shadow-sm transition-colors duration-200" %>
34
+
35
+ <%= link_to "Create New #{@model_name}", new_polymorphic_path(@model_class),
36
+ class: "inline-flex items-center px-4 py-2 bg-green-600 hover:bg-green-700 text-white font-medium rounded shadow-sm transition-colors duration-200" %>
37
+ </div>
38
+
39
+ <div>
40
+ <a href="javascript:history.back()" class="inline-flex items-center text-gray-600 hover:text-gray-900 font-medium transition-colors duration-200">
41
+ <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
42
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
43
+ </svg>
44
+ Go Back
45
+ </a>
46
+ </div>
47
+ </div>
48
+ </div>
49
+ </div>
@@ -0,0 +1,32 @@
1
+ <div class="container mx-auto px-4 py-8">
2
+ <div class="flex justify-between items-center mb-6">
3
+ <h1 class="text-3xl font-bold text-gray-900"><%= @model_name %> Details</h1>
4
+ <div class="space-x-2">
5
+ <%= link_to "Edit",
6
+ url_for(action: :edit, id: @record.id),
7
+ class: "bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" %>
8
+ <%= link_to "Back to #{@model_name.pluralize}",
9
+ url_for(action: :index),
10
+ class: "text-gray-600 hover:text-gray-900" %>
11
+ </div>
12
+ </div>
13
+
14
+ <%= render partial: 'elaine_crud/base/show_details', locals: { record: @record, columns: @columns } %>
15
+
16
+ <div class="bg-white shadow-md rounded-lg p-6 mt-6">
17
+ <div class="flex justify-end space-x-3">
18
+ <%= link_to "Edit #{@model_name}",
19
+ url_for(action: :edit, id: @record.id),
20
+ class: "bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" %>
21
+ <%= button_to "Delete #{@model_name}",
22
+ url_for(action: :destroy, id: @record.id),
23
+ method: :delete,
24
+ form: {
25
+ class: "inline",
26
+ data: { turbo: false },
27
+ onsubmit: "return confirm('Are you sure you want to delete this #{@model_name.downcase}?');"
28
+ },
29
+ class: "bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded" %>
30
+ </div>
31
+ </div>
32
+ </div>