easy-admin-rails 0.1.10 → 0.1.12

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 (33) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/builds/easy_admin.base.js +3204 -1
  3. data/app/assets/builds/easy_admin.base.js.map +4 -4
  4. data/app/assets/builds/easy_admin.css +327 -0
  5. data/app/assets/stylesheets/easy_admin/application.tailwind.css +1 -0
  6. data/app/assets/stylesheets/easy_admin/swal_customizations.css +168 -0
  7. data/app/components/easy_admin/fields/base_component.rb +23 -0
  8. data/app/components/easy_admin/fields/form/boolean_component.rb +1 -0
  9. data/app/components/easy_admin/fields/form/date_component.rb +3 -2
  10. data/app/components/easy_admin/fields/form/datetime_component.rb +3 -2
  11. data/app/components/easy_admin/fields/form/email_component.rb +3 -2
  12. data/app/components/easy_admin/fields/form/file_component.rb +6 -7
  13. data/app/components/easy_admin/fields/form/number_component.rb +3 -2
  14. data/app/components/easy_admin/fields/form/select_component.rb +5 -3
  15. data/app/components/easy_admin/fields/form/text_component.rb +3 -2
  16. data/app/components/easy_admin/fields/form/textarea_component.rb +3 -2
  17. data/app/components/easy_admin/versions/diff_component.rb +184 -0
  18. data/app/components/easy_admin/versions/diff_modal_component.rb +314 -0
  19. data/app/components/easy_admin/versions/history_component.rb +141 -0
  20. data/app/components/easy_admin/versions/pagination_component.rb +206 -0
  21. data/app/components/easy_admin/versions/timeline_component.rb +412 -0
  22. data/app/concerns/easy_admin/resource_versions.rb +306 -0
  23. data/app/controllers/easy_admin/application_controller.rb +8 -0
  24. data/app/controllers/easy_admin/resources_controller.rb +5 -3
  25. data/app/javascript/controllers/modal_controller.js +38 -0
  26. data/app/javascript/easy_admin/controllers/version_revert_controller.js +108 -0
  27. data/app/javascript/easy_admin/controllers.js +3 -1
  28. data/config/routes.rb +5 -0
  29. data/lib/easy-admin-rails.rb +1 -0
  30. data/lib/easy_admin/resource.rb +15 -0
  31. data/lib/easy_admin/version.rb +1 -1
  32. data/lib/easy_admin/versioning.rb +43 -0
  33. metadata +12 -2
@@ -0,0 +1,141 @@
1
+ module EasyAdmin
2
+ module Versions
3
+ class HistoryComponent < BaseComponent
4
+ def initialize(record:, resource_class:, versions_result: nil)
5
+ @record = record
6
+ @resource_class = resource_class
7
+ @versions_result = versions_result
8
+ end
9
+
10
+ def view_template
11
+ div(id: "versions-section", class: "space-y-6") do
12
+ if versioning_available?
13
+ if has_versions?
14
+ render_versions_section
15
+ else
16
+ render_no_versions_message
17
+ end
18
+ else
19
+ render_versioning_unavailable_message
20
+ end
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def versioning_available?
27
+ return false unless defined?(PaperTrail)
28
+ @record.respond_to?(:versions)
29
+ rescue
30
+ false
31
+ end
32
+
33
+ def has_versions?
34
+ return @versions_result.total_count > 0 if @versions_result
35
+ @record.versions.any?
36
+ end
37
+
38
+ def render_versions_section
39
+ div(class: "bg-white shadow-sm rounded-lg border") do
40
+ render_section_header
41
+ render_timeline_content
42
+ render_pagination if @versions_result&.pagy&.pages&.> 1
43
+ end
44
+ end
45
+
46
+ def render_section_header
47
+ div(class: "px-4 sm:px-6 py-4 border-b border-gray-200") do
48
+ h3(class: "text-lg font-medium text-gray-900") { "Version History" }
49
+ p(class: "mt-1 text-sm text-gray-600") do
50
+ total = @versions_result&.total_count || @record.versions.count
51
+ "#{total} versions • Showing recent changes"
52
+ end
53
+ end
54
+ end
55
+
56
+ def render_timeline_content
57
+ div(class: "p-4 sm:p-6") do
58
+ if @versions_result
59
+ # Use paginated versions from the concern
60
+ render EasyAdmin::Versions::TimelineComponent.new(
61
+ versions: @versions_result.items,
62
+ resource_class: @resource_class,
63
+ record: @record
64
+ )
65
+ else
66
+ # Fallback to loading first 10 versions directly for lazy loading
67
+ turbo_frame(id: "versions-timeline", src: versions_url, loading: "lazy") do
68
+ render_timeline_skeleton
69
+ end
70
+ end
71
+ end
72
+ end
73
+
74
+ def render_timeline_skeleton
75
+ div(class: "animate-pulse space-y-6") do
76
+ 3.times do
77
+ div(class: "bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden") do
78
+ div(class: "px-4 sm:px-6 py-4 border-b border-gray-100 bg-gray-50/50") do
79
+ div(class: "flex items-center space-x-3") do
80
+ div(class: "h-6 bg-gray-200 rounded w-20")
81
+ div(class: "h-4 bg-gray-200 rounded w-32")
82
+ end
83
+ end
84
+ div(class: "p-4 sm:p-6") do
85
+ div(class: "h-4 bg-gray-200 rounded w-full mb-2")
86
+ div(class: "h-4 bg-gray-200 rounded w-3/4")
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+
93
+ def render_pagination
94
+ render EasyAdmin::Versions::PaginationComponent.new(
95
+ pagy: @versions_result.pagy,
96
+ resource_class: @resource_class,
97
+ record: @record
98
+ )
99
+ end
100
+
101
+ def render_no_versions_message
102
+ div(class: "text-center py-12 bg-gray-50 rounded-lg") do
103
+ div(class: "text-gray-400 mb-4") do
104
+ unsafe_raw <<~SVG
105
+ <svg class="mx-auto h-12 w-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
106
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
107
+ </svg>
108
+ SVG
109
+ end
110
+ h3(class: "text-lg font-medium text-gray-900 mb-2") { "No Version History" }
111
+ p(class: "text-gray-600") { "No changes have been tracked for this record yet." }
112
+ end
113
+ end
114
+
115
+ def render_versioning_unavailable_message
116
+ div(class: "text-center py-12 bg-yellow-50 rounded-lg border border-yellow-200") do
117
+ div(class: "text-yellow-400 mb-4") do
118
+ unsafe_raw <<~SVG
119
+ <svg class="mx-auto h-12 w-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
120
+ <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" />
121
+ </svg>
122
+ SVG
123
+ end
124
+ h3(class: "text-lg font-medium text-gray-900 mb-2") { "Versioning Not Available" }
125
+ p(class: "text-gray-600") do
126
+ "Version tracking is not configured for this resource. Add "
127
+ code(class: "bg-gray-100 px-2 py-1 rounded text-sm") { "has_paper_trail" }
128
+ " to the model to enable version history."
129
+ end
130
+ end
131
+ end
132
+
133
+ def versions_url
134
+ easy_admin_url_helpers.resource_versions_path(
135
+ @resource_class.route_key,
136
+ @record.id
137
+ )
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,206 @@
1
+ module EasyAdmin
2
+ module Versions
3
+ class PaginationComponent < BaseComponent
4
+ def initialize(pagy:, resource_class:, record:)
5
+ @pagy = pagy
6
+ @resource_class = resource_class
7
+ @record = record
8
+ end
9
+
10
+ def view_template
11
+ return unless @pagy.pages > 1
12
+
13
+ div(id: "versions-pagination", class: "mt-6 flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6") do
14
+ # Mobile pagination info
15
+ div(class: "flex flex-1 justify-between sm:hidden") do
16
+ if @pagy.prev
17
+ link_to_previous_page
18
+ else
19
+ span(class: "relative inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-500") do
20
+ "Previous"
21
+ end
22
+ end
23
+
24
+ if @pagy.next
25
+ link_to_next_page
26
+ else
27
+ span(class: "relative ml-3 inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-500") do
28
+ "Next"
29
+ end
30
+ end
31
+ end
32
+
33
+ # Desktop pagination
34
+ div(class: "hidden sm:flex sm:flex-1 sm:items-center sm:justify-between") do
35
+ # Results info
36
+ div do
37
+ p(class: "text-sm text-gray-700") do
38
+ plain "Showing "
39
+ span(class: "font-medium") { @pagy.from }
40
+ plain " to "
41
+ span(class: "font-medium") { @pagy.to }
42
+ plain " of "
43
+ span(class: "font-medium") { @pagy.count }
44
+ plain " versions"
45
+ end
46
+ end
47
+
48
+ # Page navigation
49
+ div do
50
+ nav(class: "isolate inline-flex -space-x-px rounded-md shadow-sm", "aria-label": "Pagination") do
51
+ # Previous page
52
+ if @pagy.prev
53
+ link_to_previous_page_desktop
54
+ else
55
+ span(class: "relative inline-flex items-center rounded-l-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0") do
56
+ span(class: "sr-only") { "Previous" }
57
+ unsafe_raw <<~SVG
58
+ <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
59
+ <path fill-rule="evenodd" d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z" clip-rule="evenodd"/>
60
+ </svg>
61
+ SVG
62
+ end
63
+ end
64
+
65
+ # Page numbers (show first, current range, and last)
66
+ render_page_numbers
67
+
68
+ # Next page
69
+ if @pagy.next
70
+ link_to_next_page_desktop
71
+ else
72
+ span(class: "relative inline-flex items-center rounded-r-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0") do
73
+ span(class: "sr-only") { "Next" }
74
+ unsafe_raw <<~SVG
75
+ <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
76
+ <path fill-rule="evenodd" d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clip-rule="evenodd"/>
77
+ </svg>
78
+ SVG
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+
87
+ private
88
+
89
+ def link_to_previous_page
90
+ a(
91
+ href: versions_url(page: @pagy.prev),
92
+ data: { turbo_frame: "versions-section" },
93
+ class: "relative inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
94
+ ) do
95
+ "Previous"
96
+ end
97
+ end
98
+
99
+ def link_to_next_page
100
+ a(
101
+ href: versions_url(page: @pagy.next),
102
+ data: { turbo_frame: "versions-section" },
103
+ class: "relative ml-3 inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
104
+ ) do
105
+ "Next"
106
+ end
107
+ end
108
+
109
+ def link_to_previous_page_desktop
110
+ a(
111
+ href: versions_url(page: @pagy.prev),
112
+ data: { turbo_frame: "versions-section" },
113
+ class: "relative inline-flex items-center rounded-l-md px-2 py-2 text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0"
114
+ ) do
115
+ span(class: "sr-only") { "Previous" }
116
+ unsafe_raw <<~SVG
117
+ <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
118
+ <path fill-rule="evenodd" d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z" clip-rule="evenodd"/>
119
+ </svg>
120
+ SVG
121
+ end
122
+ end
123
+
124
+ def link_to_next_page_desktop
125
+ a(
126
+ href: versions_url(page: @pagy.next),
127
+ data: { turbo_frame: "versions-section" },
128
+ class: "relative inline-flex items-center rounded-r-md px-2 py-2 text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0"
129
+ ) do
130
+ span(class: "sr-only") { "Next" }
131
+ unsafe_raw <<~SVG
132
+ <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
133
+ <path fill-rule="evenodd" d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clip-rule="evenodd"/>
134
+ </svg>
135
+ SVG
136
+ end
137
+ end
138
+
139
+ def render_page_numbers
140
+ # Calculate range of pages to show
141
+ current = @pagy.page
142
+ total = @pagy.pages
143
+
144
+ # Show pages around current page
145
+ start_page = [current - 2, 1].max
146
+ end_page = [current + 2, total].min
147
+
148
+ # Always show page 1 if not in range
149
+ if start_page > 1
150
+ render_page_link(1)
151
+ if start_page > 2
152
+ span(class: "relative inline-flex items-center px-4 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 focus:outline-offset-0") do
153
+ "..."
154
+ end
155
+ end
156
+ end
157
+
158
+ # Show page range
159
+ (start_page..end_page).each do |page|
160
+ if page == current
161
+ render_current_page(page)
162
+ else
163
+ render_page_link(page)
164
+ end
165
+ end
166
+
167
+ # Always show last page if not in range
168
+ if end_page < total
169
+ if end_page < total - 1
170
+ span(class: "relative inline-flex items-center px-4 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 focus:outline-offset-0") do
171
+ "..."
172
+ end
173
+ end
174
+ render_page_link(total)
175
+ end
176
+ end
177
+
178
+ def render_current_page(page)
179
+ span(
180
+ "aria-current": "page",
181
+ class: "relative z-10 inline-flex items-center bg-blue-600 px-4 py-2 text-sm font-semibold text-white focus:z-20 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
182
+ ) do
183
+ page.to_s
184
+ end
185
+ end
186
+
187
+ def render_page_link(page)
188
+ a(
189
+ href: versions_url(page: page),
190
+ data: { turbo_frame: "versions-section" },
191
+ class: "relative inline-flex items-center px-4 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0"
192
+ ) do
193
+ page.to_s
194
+ end
195
+ end
196
+
197
+ def versions_url(page:)
198
+ easy_admin_url_helpers.resource_versions_path(
199
+ @resource_class.route_key,
200
+ @record.id,
201
+ page: page
202
+ )
203
+ end
204
+ end
205
+ end
206
+ end