easy-admin-rails 0.1.11 → 0.1.13

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.
@@ -0,0 +1,306 @@
1
+ module EasyAdmin
2
+ module ResourceVersions
3
+ extend ActiveSupport::Concern
4
+
5
+ DEFAULT_VERSIONS_PER_PAGE = 10
6
+ MAX_VERSIONS_PER_PAGE = 50
7
+
8
+ included do
9
+ # Include in controllers that need version functionality
10
+ end
11
+
12
+ # Load versions for a specific record with pagination and turbo compatibility
13
+ def load_record_versions(page: 1, per_page: DEFAULT_VERSIONS_PER_PAGE)
14
+ return OpenStruct.new(items: [], total_count: 0, has_more: false, current_page: 1) unless record_has_versions?(@record)
15
+
16
+ per_page = [per_page.to_i, MAX_VERSIONS_PER_PAGE].min
17
+ per_page = DEFAULT_VERSIONS_PER_PAGE if per_page <= 0
18
+ page = [page.to_i, 1].max
19
+
20
+ # Use pagy for consistent pagination across the app
21
+ pagy, versions = pagy(@record.versions.reorder(id: :desc).includes(:item),
22
+ items: per_page,
23
+ limit: per_page,
24
+ page: page,
25
+ overflow: :last_page)
26
+
27
+ OpenStruct.new(
28
+ items: versions,
29
+ pagy: pagy,
30
+ total_count: pagy.count,
31
+ has_more: pagy.next.present?,
32
+ current_page: pagy.page,
33
+ per_page: pagy.vars[:items],
34
+ total_pages: pagy.last
35
+ )
36
+ end
37
+
38
+ # Check if a record has version support
39
+ def record_has_versions?(record)
40
+ return false unless record
41
+ return false unless defined?(PaperTrail)
42
+
43
+ record.respond_to?(:versions) && record.class.respond_to?(:paper_trail) rescue false
44
+ end
45
+
46
+ # Get version statistics for a record
47
+ def version_statistics(record)
48
+ return {} unless record_has_versions?(record)
49
+
50
+ versions = record.versions
51
+
52
+ {
53
+ total_versions: versions.count,
54
+ create_count: versions.where(event: 'create').count,
55
+ update_count: versions.where(event: 'update').count,
56
+ destroy_count: versions.where(event: 'destroy').count,
57
+ first_version_at: versions.order(:created_at).first&.created_at,
58
+ last_version_at: versions.order(:created_at).last&.created_at,
59
+ unique_users: versions.where.not(whodunnit: nil).distinct.count(:whodunnit)
60
+ }
61
+ end
62
+
63
+ # Turbo-compatible version loading action
64
+ def versions
65
+ result = load_record_versions(
66
+ page: params[:page] || 1,
67
+ per_page: params[:per_page] || DEFAULT_VERSIONS_PER_PAGE
68
+ )
69
+
70
+ respond_to do |format|
71
+ format.html do
72
+ if turbo_frame_request?
73
+ # For lazy loading turbo frame (versions-timeline)
74
+ render plain: EasyAdmin::Versions::TimelineComponent.new(
75
+ versions: result.items,
76
+ turbo_frame_id: "versions-timeline",
77
+ resource_class: @resource_class,
78
+ record: @record
79
+ ).call
80
+ else
81
+ # Regular page load - redirect or render full page
82
+ redirect_to easy_admin.resource_path(@resource_class.route_key, @record, anchor: "versions")
83
+ end
84
+ end
85
+
86
+ format.turbo_stream do
87
+ if params[:append] == "true"
88
+ # Infinite scroll - append more versions
89
+ render turbo_stream: [
90
+ turbo_stream.append("versions-container",
91
+ EasyAdmin::Versions::TimelineComponent.new(
92
+ versions: result.items
93
+ ).call
94
+ ),
95
+ turbo_stream.replace("versions-pagination",
96
+ EasyAdmin::Versions::PaginationComponent.new(
97
+ pagy: result.pagy,
98
+ resource_class: @resource_class,
99
+ record: @record
100
+ ).call
101
+ )
102
+ ]
103
+ else
104
+ # Replace entire versions section
105
+ render turbo_stream: turbo_stream.replace("versions-section",
106
+ EasyAdmin::Versions::HistoryComponent.new(
107
+ record: @record,
108
+ resource_class: @resource_class,
109
+ versions_result: result
110
+ ).call
111
+ )
112
+ end
113
+ end
114
+ end
115
+ end
116
+
117
+ # Revert a record to a specific version with turbo response
118
+ def revert_version
119
+ version_id = params[:version_id]
120
+ version = @record.versions.find_by(id: version_id)
121
+
122
+ unless version
123
+ respond_to do |format|
124
+ format.turbo_stream do
125
+ render turbo_stream: turbo_stream.update("notifications",
126
+ EasyAdmin::NotificationComponent.new(
127
+ type: :error,
128
+ message: "Version not found",
129
+ title: "Error"
130
+ ).call
131
+ )
132
+ end
133
+ format.html do
134
+ redirect_to easy_admin.resource_path(@resource_class.route_key, @record),
135
+ alert: "Version not found"
136
+ end
137
+ end
138
+ return
139
+ end
140
+
141
+ result = revert_to_version(@record, version_id, current_user: current_admin_user)
142
+
143
+ respond_to do |format|
144
+ if result[:success]
145
+ format.turbo_stream do
146
+ render turbo_stream: [
147
+ turbo_stream.update("notifications",
148
+ EasyAdmin::NotificationComponent.new(
149
+ type: :success,
150
+ message: result[:message],
151
+ title: "Success"
152
+ ).call
153
+ ),
154
+ # Refresh the versions section
155
+ turbo_stream.replace("versions-section",
156
+ EasyAdmin::Versions::HistoryComponent.new(
157
+ record: @record.reload,
158
+ resource_class: @resource_class
159
+ ).call
160
+ ),
161
+ # Update the main record display if on show page
162
+ turbo_stream.replace("record-content",
163
+ render_record_content(@record.reload)
164
+ )
165
+ ]
166
+ end
167
+ format.html do
168
+ redirect_to easy_admin.resource_path(@resource_class.route_key, @record),
169
+ notice: result[:message]
170
+ end
171
+ else
172
+ format.turbo_stream do
173
+ render turbo_stream: turbo_stream.update("notifications",
174
+ EasyAdmin::NotificationComponent.new(
175
+ type: :error,
176
+ message: result[:error],
177
+ title: "Error"
178
+ ).call
179
+ )
180
+ end
181
+ format.html do
182
+ redirect_to easy_admin.resource_path(@resource_class.route_key, @record),
183
+ alert: result[:error]
184
+ end
185
+ end
186
+ end
187
+ end
188
+
189
+ # Show detailed diff modal for a version
190
+ def version_diff
191
+ version = @record.versions.find_by(id: params[:version_id])
192
+
193
+ unless version
194
+ respond_to do |format|
195
+ format.html do
196
+ render plain: EasyAdmin::Versions::ErrorComponent.new(
197
+ message: "Version not found"
198
+ ).call
199
+ end
200
+ end
201
+ return
202
+ end
203
+
204
+ respond_to do |format|
205
+ format.html do
206
+ render plain: EasyAdmin::Versions::DiffModalComponent.new(
207
+ version: version,
208
+ record: @record,
209
+ resource_class: @resource_class
210
+ ).call
211
+ end
212
+ end
213
+ end
214
+
215
+ private
216
+
217
+ # Revert a record to a specific version
218
+ def revert_to_version(record, version_id, current_user: nil)
219
+ return { success: false, error: "Versioning not available" } unless record_has_versions?(record)
220
+
221
+ version = record.versions.find_by(id: version_id)
222
+ return { success: false, error: "Version not found" } unless version
223
+
224
+ begin
225
+ # Set whodunnit for the revert action
226
+ PaperTrail.request.whodunnit = current_user&.email if current_user
227
+
228
+ # Parse version object manually with safe YAML loading
229
+ old_attributes = parse_version_object_safely(version.object)
230
+ return { success: false, error: "Cannot reify this version" } unless old_attributes.is_a?(Hash)
231
+
232
+ # Filter out attributes that shouldn't be reverted
233
+ filtered_attributes = old_attributes.except('id', 'created_at', 'updated_at')
234
+
235
+ if record.update(filtered_attributes)
236
+ {
237
+ success: true,
238
+ message: "Successfully reverted to version ##{version_id}",
239
+ reverted_version: version,
240
+ new_version: record.versions.last
241
+ }
242
+ else
243
+ {
244
+ success: false,
245
+ error: "Failed to revert: #{record.errors.full_messages.join(', ')}",
246
+ validation_errors: record.errors
247
+ }
248
+ end
249
+ rescue => e
250
+ {
251
+ success: false,
252
+ error: "Error during revert: #{e.message}"
253
+ }
254
+ ensure
255
+ PaperTrail.request.whodunnit = nil
256
+ end
257
+ end
258
+
259
+ # Helper to render record content (override in controller if needed)
260
+ def render_record_content(record)
261
+ # This should be implemented in the controller that includes this concern
262
+ # to return the appropriate component for the record display
263
+ ""
264
+ end
265
+
266
+ private
267
+
268
+ # Safe YAML parsing for version objects with proper class permissions
269
+ def parse_version_object_safely(object_yaml)
270
+ return {} if object_yaml.blank?
271
+
272
+ YAML.safe_load(
273
+ object_yaml,
274
+ permitted_classes: [Time, Date, DateTime, ActiveSupport::TimeWithZone, ActiveSupport::TimeZone, Symbol],
275
+ aliases: true
276
+ )
277
+ rescue Psych::DisallowedClass, Psych::BadAlias
278
+ begin
279
+ YAML.unsafe_load(object_yaml)
280
+ rescue
281
+ extract_basic_attributes_from_yaml(object_yaml)
282
+ end
283
+ rescue => e
284
+ Rails.logger.warn "Failed to parse version object for revert: #{e.message}"
285
+ {}
286
+ end
287
+
288
+ # Fallback method to extract basic attributes from YAML string
289
+ def extract_basic_attributes_from_yaml(yaml_string)
290
+ attributes = {}
291
+
292
+ yaml_string.scan(/^(\w+):\s*(.+)$/).each do |key, value|
293
+ # Skip complex objects
294
+ next if value.include?('!ruby/object')
295
+
296
+ # Clean up the value
297
+ cleaned_value = value.strip.gsub(/^["']|["']$/, '')
298
+ attributes[key] = cleaned_value unless cleaned_value.empty?
299
+ end
300
+
301
+ attributes
302
+ rescue
303
+ {}
304
+ end
305
+ end
306
+ end
@@ -8,6 +8,7 @@ module EasyAdmin
8
8
  before_action :authenticate_easy_admin_admin_user!
9
9
  before_action :set_active_storage_url_options
10
10
  before_action :set_feature_toggles
11
+ before_action :set_paper_trail_whodunnit
11
12
 
12
13
  protected
13
14
 
@@ -61,6 +62,13 @@ module EasyAdmin
61
62
  descriptions[feature_name.to_s] || "Toggle #{feature_name.humanize.downcase}"
62
63
  end
63
64
 
65
+ # Set Paper Trail whodunnit to track which admin user made changes
66
+ def set_paper_trail_whodunnit
67
+ if defined?(PaperTrail) && current_admin_user
68
+ PaperTrail.request.whodunnit = current_admin_user.email
69
+ end
70
+ end
71
+
64
72
  helper_method :current_admin_user, :admin_user_signed_in?
65
73
  end
66
74
  end
@@ -1,7 +1,9 @@
1
1
  module EasyAdmin
2
2
  class ResourcesController < ApplicationController
3
+ include EasyAdmin::ResourceVersions
4
+
3
5
  before_action :load_resource_class
4
- before_action :load_record, only: [:show, :edit, :update, :destroy, :edit_field, :update_field, :belongs_to_reattach, :belongs_to_edit_attached, :update_belongs_to_attached]
6
+ before_action :load_record, only: [:show, :edit, :update, :destroy, :edit_field, :update_field, :belongs_to_reattach, :belongs_to_edit_attached, :update_belongs_to_attached, :versions, :revert_version, :version_diff]
5
7
 
6
8
  def index
7
9
  # Set up data needed for both HTML and turbo stream
@@ -0,0 +1,38 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ connect() {
5
+ // Disable body scroll when modal opens
6
+ document.body.style.overflow = 'hidden'
7
+
8
+ // Focus trap - focus the modal
9
+ this.element.focus()
10
+ }
11
+
12
+ disconnect() {
13
+ // Re-enable body scroll when modal closes
14
+ document.body.style.overflow = ''
15
+ }
16
+
17
+ close() {
18
+ // Clear the modal turbo frame
19
+ const modalFrame = document.getElementById("modal")
20
+ if (modalFrame) {
21
+ modalFrame.innerHTML = ""
22
+ }
23
+ }
24
+
25
+ handleOutsideClick(event) {
26
+ // Close modal if clicking outside the modal container
27
+ if (event.target === this.element) {
28
+ this.close()
29
+ }
30
+ }
31
+
32
+ // Handle escape key
33
+ keydown(event) {
34
+ if (event.key === "Escape") {
35
+ this.close()
36
+ }
37
+ }
38
+ }
@@ -0,0 +1,108 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+ import { FetchRequest } from "@rails/request.js"
3
+ import Swal from "sweetalert2"
4
+
5
+ export default class extends Controller {
6
+ static values = {
7
+ versionId: Number,
8
+ resourceId: String,
9
+ resourceName: String
10
+ }
11
+
12
+ confirm(event) {
13
+ event.preventDefault()
14
+
15
+ Swal.fire({
16
+ title: 'Revert to Version?',
17
+ html: `Are you sure you want to revert to version <strong>#${this.versionIdValue}</strong>?<br><br>This will restore the old data and create a new version entry.`,
18
+ icon: 'question',
19
+ showCancelButton: true,
20
+ confirmButtonText: 'Yes, Revert',
21
+ cancelButtonText: 'Cancel',
22
+ reverseButtons: true,
23
+ focusCancel: true,
24
+ ...this.getAdminPanelStyles()
25
+ }).then((result) => {
26
+ if (result.isConfirmed) {
27
+ this.performRevert()
28
+ }
29
+ })
30
+ }
31
+
32
+ async performRevert() {
33
+ const url = `/admin/${this.resourceNameValue}/${this.resourceIdValue}/versions/${this.versionIdValue}/revert`
34
+
35
+ // Show loading indicator
36
+ Swal.fire({
37
+ title: 'Reverting...',
38
+ text: `Restoring version #${this.versionIdValue}`,
39
+ allowOutsideClick: false,
40
+ allowEscapeKey: false,
41
+ showConfirmButton: false,
42
+ didOpen: () => {
43
+ Swal.showLoading()
44
+ },
45
+ ...this.getAdminPanelStyles()
46
+ })
47
+
48
+ try {
49
+ const request = new FetchRequest("post", url, {
50
+ headers: {
51
+ "X-CSRF-Token": document.querySelector('meta[name="csrf-token"]').content,
52
+ "Accept": "text/vnd.turbo-stream.html"
53
+ }
54
+ })
55
+
56
+ const response = await request.perform()
57
+
58
+ if (response.ok) {
59
+ // Show success message then reload
60
+ Swal.fire({
61
+ title: 'Success!',
62
+ text: `Reverted to version #${this.versionIdValue}`,
63
+ icon: 'success',
64
+ timer: 1500,
65
+ showConfirmButton: false,
66
+ ...this.getAdminPanelStyles()
67
+ }).then(() => {
68
+ window.location.reload()
69
+ })
70
+ } else {
71
+ console.error("Revert failed:", response.statusText)
72
+ this.showError("Failed to revert. Please try again.")
73
+ }
74
+ } catch (error) {
75
+ console.error("Revert error:", error)
76
+ this.showError("An error occurred while reverting. Please try again.")
77
+ }
78
+ }
79
+
80
+ showError(message) {
81
+ Swal.fire({
82
+ title: 'Error',
83
+ text: message,
84
+ icon: 'error',
85
+ confirmButtonText: 'OK',
86
+ ...this.getAdminPanelStyles()
87
+ })
88
+ }
89
+
90
+ getAdminPanelStyles() {
91
+ return {
92
+ buttonsStyling: false,
93
+ customClass: {
94
+ popup: 'easy-admin-swal-popup',
95
+ title: 'easy-admin-swal-title',
96
+ htmlContainer: 'easy-admin-swal-content',
97
+ confirmButton: 'easy-admin-swal-confirm-button',
98
+ cancelButton: 'easy-admin-swal-cancel-button',
99
+ actions: 'easy-admin-swal-actions'
100
+ },
101
+ background: '#ffffff',
102
+ backdrop: 'rgba(0, 0, 0, 0.4)',
103
+ width: '28rem',
104
+ padding: '0',
105
+ borderRadius: '0.75rem'
106
+ }
107
+ }
108
+ }
@@ -25,6 +25,7 @@ import ContextMenuController from './controllers/context_menu_controller'
25
25
  import RowModalController from './controllers/row_modal_controller'
26
26
  import RowActionController from './controllers/row_action_controller'
27
27
  import JsoneditorController from './controllers/jsoneditor_controller'
28
+ import VersionRevertController from './controllers/version_revert_controller'
28
29
 
29
30
  // Register controllers
30
31
  application.register('sidebar', SidebarController)
@@ -51,4 +52,5 @@ application.register('confirmation-modal', ConfirmationModalController)
51
52
  application.register('context-menu', ContextMenuController)
52
53
  application.register('row-modal', RowModalController)
53
54
  application.register('row-action', RowActionController)
54
- application.register('jsoneditor', JsoneditorController)
55
+ application.register('jsoneditor', JsoneditorController)
56
+ application.register('version-revert', VersionRevertController)
data/config/routes.rb CHANGED
@@ -66,5 +66,10 @@ EasyAdmin::Engine.routes.draw do
66
66
  get '/:id/metrics/:metric_id', to: 'resources#metric', as: 'resource_metric'
67
67
  get '/:id/charts/:chart_id', to: 'resources#chart', as: 'resource_chart'
68
68
  get '/:id/tabs/:tab_name', to: 'resources#tab_content', as: 'resource_tab_content'
69
+
70
+ # Version management routes
71
+ get '/:id/versions', to: 'resources#versions', as: 'resource_versions'
72
+ post '/:id/versions/:version_id/revert', to: 'resources#revert_version', as: 'revert_resource_version'
73
+ get '/:id/versions/:version_id/diff', to: 'resources#version_diff', as: 'resource_version_diff'
69
74
  end
70
75
  end
@@ -4,6 +4,7 @@ require "ostruct"
4
4
  require "easy_admin/version"
5
5
  require "easy_admin/configuration"
6
6
  require "easy_admin/resource_registry"
7
+ require "easy_admin/versioning"
7
8
  require "easy_admin/resource"
8
9
  require "easy_admin/dashboard_registry"
9
10
  require "easy_admin/dashboard"
@@ -1,5 +1,7 @@
1
1
  module EasyAdmin
2
2
  class Resource
3
+ include EasyAdmin::Versioning
4
+
3
5
  class_attribute :model_class_name, :resource_name, :fields_config, :form_tabs_config, :show_layout_config, :scopes_config, :pagination_config, :includes_config, :batch_actions_config, :batch_actions_enabled, :row_actions_config, :custom_title
4
6
 
5
7
  def self.inherited(subclass)
@@ -822,5 +824,18 @@ module EasyAdmin
822
824
  def self.param_key
823
825
  resource_name.underscore
824
826
  end
827
+
828
+ # Versioning DSL - Simple API for enabling version viewing
829
+ def self.versioning(enabled = true, **options)
830
+ if enabled
831
+ enable_versioning(options)
832
+ else
833
+ self.versioning_enabled = false
834
+ end
835
+ end
836
+
837
+ def self.has_versioning?
838
+ model_has_versions?
839
+ end
825
840
  end
826
841
  end
@@ -1,3 +1,3 @@
1
1
  module EasyAdmin
2
- VERSION = "0.1.11"
2
+ VERSION = "0.1.13"
3
3
  end
@@ -0,0 +1,43 @@
1
+ module EasyAdmin
2
+ module Versioning
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ class_attribute :versioning_enabled, default: false
7
+ class_attribute :versioning_options, default: {}
8
+ end
9
+
10
+ class_methods do
11
+ def enable_versioning(options = {})
12
+ self.versioning_enabled = true
13
+ self.versioning_options = options
14
+ end
15
+
16
+ def versioning_enabled?
17
+ versioning_enabled && defined?(PaperTrail)
18
+ end
19
+
20
+ def versioning_config
21
+ return nil unless versioning_enabled?
22
+
23
+ {
24
+ enabled: true,
25
+ diff_display: versioning_options[:diff_display] || :inline,
26
+ show_whodunnit: versioning_options[:show_whodunnit] != false,
27
+ show_metadata: versioning_options[:show_metadata] != false,
28
+ limit: versioning_options[:limit] || 50 # Default to showing last 50 versions
29
+ }
30
+ end
31
+
32
+ # Check if a model has versioning available
33
+ def model_has_versions?
34
+ return false unless versioning_enabled?
35
+
36
+ # Check if the model has paper_trail and instances respond to versions
37
+ model_class.respond_to?(:paper_trail) && model_class.new.respond_to?(:versions)
38
+ rescue
39
+ false
40
+ end
41
+ end
42
+ end
43
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: easy-admin-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.11
4
+ version: 0.1.13
5
5
  platform: ruby
6
6
  authors:
7
7
  - Slaurmagan
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-09-02 00:00:00.000000000 Z
11
+ date: 2025-09-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -195,6 +195,7 @@ files:
195
195
  - app/assets/config/easy_admin_manifest.js
196
196
  - app/assets/images/jsoneditor-icons.svg
197
197
  - app/assets/stylesheets/easy_admin/application.tailwind.css
198
+ - app/assets/stylesheets/easy_admin/swal_customizations.css
198
199
  - app/components/easy_admin/base_component.rb
199
200
  - app/components/easy_admin/batch_action_bar_component.rb
200
201
  - app/components/easy_admin/batch_action_form_component.rb
@@ -283,6 +284,12 @@ files:
283
284
  - app/components/easy_admin/sidebar_component.rb
284
285
  - app/components/easy_admin/turbo/response_component.rb
285
286
  - app/components/easy_admin/turbo/stream_component.rb
287
+ - app/components/easy_admin/versions/diff_component.rb
288
+ - app/components/easy_admin/versions/diff_modal_component.rb
289
+ - app/components/easy_admin/versions/history_component.rb
290
+ - app/components/easy_admin/versions/pagination_component.rb
291
+ - app/components/easy_admin/versions/timeline_component.rb
292
+ - app/concerns/easy_admin/resource_versions.rb
286
293
  - app/controllers/easy_admin/application_controller.rb
287
294
  - app/controllers/easy_admin/batch_actions_controller.rb
288
295
  - app/controllers/easy_admin/confirmation_modal_controller.rb
@@ -299,6 +306,7 @@ files:
299
306
  - app/helpers/easy_admin/fields_helper.rb
300
307
  - app/helpers/easy_admin/pagy_helper.rb
301
308
  - app/helpers/easy_admin/resources_helper.rb
309
+ - app/javascript/controllers/modal_controller.js
302
310
  - app/javascript/easy_admin.base.js
303
311
  - app/javascript/easy_admin/application.js
304
312
  - app/javascript/easy_admin/controllers.js
@@ -332,6 +340,7 @@ files:
332
340
  - app/javascript/easy_admin/controllers/table_row_controller.js
333
341
  - app/javascript/easy_admin/controllers/toggle_switch_controller.js
334
342
  - app/javascript/easy_admin/controllers/turbo_stream_redirect.js
343
+ - app/javascript/easy_admin/controllers/version_revert_controller.js
335
344
  - app/models/easy_admin/admin_user.rb
336
345
  - app/models/easy_admin/application_record.rb
337
346
  - app/views/easy_admin/dashboard/index.html.erb
@@ -370,6 +379,7 @@ files:
370
379
  - lib/easy_admin/resource_registry.rb
371
380
  - lib/easy_admin/types/json_type.rb
372
381
  - lib/easy_admin/version.rb
382
+ - lib/easy_admin/versioning.rb
373
383
  - lib/generators/easy_admin/auth_generator.rb
374
384
  - lib/generators/easy_admin/card/card_generator.rb
375
385
  - lib/generators/easy_admin/card/templates/card_component.rb.erb