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.
- checksums.yaml +4 -4
- data/app/assets/builds/easy_admin.base.js +3204 -1
- data/app/assets/builds/easy_admin.base.js.map +4 -4
- data/app/assets/builds/easy_admin.css +327 -0
- data/app/assets/stylesheets/easy_admin/application.tailwind.css +1 -0
- data/app/assets/stylesheets/easy_admin/swal_customizations.css +168 -0
- data/app/components/easy_admin/fields/base_component.rb +23 -0
- data/app/components/easy_admin/fields/form/boolean_component.rb +1 -0
- data/app/components/easy_admin/fields/form/date_component.rb +3 -2
- data/app/components/easy_admin/fields/form/datetime_component.rb +3 -2
- data/app/components/easy_admin/fields/form/email_component.rb +3 -2
- data/app/components/easy_admin/fields/form/file_component.rb +6 -7
- data/app/components/easy_admin/fields/form/number_component.rb +3 -2
- data/app/components/easy_admin/fields/form/select_component.rb +5 -3
- data/app/components/easy_admin/fields/form/text_component.rb +3 -2
- data/app/components/easy_admin/fields/form/textarea_component.rb +3 -2
- data/app/components/easy_admin/versions/diff_component.rb +184 -0
- data/app/components/easy_admin/versions/diff_modal_component.rb +314 -0
- data/app/components/easy_admin/versions/history_component.rb +141 -0
- data/app/components/easy_admin/versions/pagination_component.rb +206 -0
- data/app/components/easy_admin/versions/timeline_component.rb +412 -0
- data/app/concerns/easy_admin/resource_versions.rb +306 -0
- data/app/controllers/easy_admin/application_controller.rb +8 -0
- data/app/controllers/easy_admin/resources_controller.rb +5 -3
- data/app/javascript/controllers/modal_controller.js +38 -0
- data/app/javascript/easy_admin/controllers/version_revert_controller.js +108 -0
- data/app/javascript/easy_admin/controllers.js +3 -1
- data/config/routes.rb +5 -0
- data/lib/easy-admin-rails.rb +1 -0
- data/lib/easy_admin/resource.rb +15 -0
- data/lib/easy_admin/version.rb +1 -1
- data/lib/easy_admin/versioning.rb +43 -0
- 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
|