You need to sign in or sign up before continuing.

paper_trail_history 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 (33) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +188 -0
  4. data/Rakefile +29 -0
  5. data/app/assets/stylesheets/paper_trail_history/application.css +93 -0
  6. data/app/controllers/paper_trail_history/application_controller.rb +6 -0
  7. data/app/controllers/paper_trail_history/models_controller.rb +57 -0
  8. data/app/controllers/paper_trail_history/records_controller.rb +58 -0
  9. data/app/controllers/paper_trail_history/versions_controller.rb +33 -0
  10. data/app/helpers/paper_trail_history/application_helper.rb +7 -0
  11. data/app/jobs/paper_trail_history/application_job.rb +6 -0
  12. data/app/mailers/paper_trail_history/application_mailer.rb +9 -0
  13. data/app/models/paper_trail_history/application_record.rb +8 -0
  14. data/app/models/paper_trail_history/trackable_model.rb +112 -0
  15. data/app/models/paper_trail_history/version_decorator.rb +104 -0
  16. data/app/models/paper_trail_history/version_service.rb +158 -0
  17. data/app/views/layouts/paper_trail_history/application.html.erb +64 -0
  18. data/app/views/paper_trail_history/models/index.html.erb +65 -0
  19. data/app/views/paper_trail_history/models/show.html.erb +49 -0
  20. data/app/views/paper_trail_history/models/versions.html.erb +30 -0
  21. data/app/views/paper_trail_history/records/show.html.erb +57 -0
  22. data/app/views/paper_trail_history/records/versions.html.erb +36 -0
  23. data/app/views/paper_trail_history/shared/_version_filters.html.erb +54 -0
  24. data/app/views/paper_trail_history/shared/_versions_table.html.erb +60 -0
  25. data/app/views/paper_trail_history/versions/show.html.erb +134 -0
  26. data/config/locales/de.yml +16 -0
  27. data/config/locales/en.yml +16 -0
  28. data/config/routes.rb +23 -0
  29. data/lib/paper_trail_history/engine.rb +8 -0
  30. data/lib/paper_trail_history/version.rb +5 -0
  31. data/lib/paper_trail_history.rb +15 -0
  32. data/lib/tasks/paper_trail_history_tasks.rake +6 -0
  33. metadata +106 -0
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaperTrailHistory
4
+ # Decorator for PaperTrail::Version objects providing formatted display methods
5
+ class VersionDecorator
6
+ attr_reader :version
7
+
8
+ # Delegate common version methods to the underlying version object
9
+ delegate :id, :event, :item_type, :item_id, :whodunnit, :object, :object_changes,
10
+ :created_at, :updated_at, :item, :changeset, to: :version
11
+
12
+ def initialize(version)
13
+ @version = version
14
+ end
15
+
16
+ def self.decorate(version)
17
+ return version if version.is_a?(VersionDecorator)
18
+
19
+ new(version)
20
+ end
21
+
22
+ def self.decorate_collection(versions)
23
+ versions.map { |version| decorate(version) }
24
+ end
25
+
26
+ def formatted_created_at
27
+ version.created_at.strftime('%B %d, %Y at %I:%M %p')
28
+ end
29
+
30
+ def event_label
31
+ case version.event
32
+ when 'create'
33
+ I18n.t('paper_trail_history.events.created')
34
+ when 'update'
35
+ I18n.t('paper_trail_history.events.updated')
36
+ when 'destroy'
37
+ I18n.t('paper_trail_history.events.deleted')
38
+ else
39
+ version.event.humanize
40
+ end
41
+ end
42
+
43
+ def event_class
44
+ case version.event
45
+ when 'create'
46
+ 'success'
47
+ when 'update'
48
+ 'warning'
49
+ when 'destroy'
50
+ 'danger'
51
+ else
52
+ 'info'
53
+ end
54
+ end
55
+
56
+ def whodunnit_display
57
+ version.whodunnit || I18n.t('paper_trail_history.actors.system')
58
+ end
59
+
60
+ def changed_attributes
61
+ return [] unless changeset
62
+
63
+ changeset.map do |attr, (old_val, new_val)|
64
+ {
65
+ attribute: attr,
66
+ old_value: format_value(old_val),
67
+ new_value: format_value(new_val)
68
+ }
69
+ end
70
+ end
71
+
72
+ def can_restore?
73
+ version.event != 'create'
74
+ end
75
+
76
+ def item_display_name
77
+ if version.item
78
+ version.item.try(:name) || version.item.try(:title) || "#{version.item_type} ##{version.item_id}"
79
+ else
80
+ "#{version.item_type} ##{version.item_id} (deleted)"
81
+ end
82
+ end
83
+
84
+ private
85
+
86
+ def format_value(value)
87
+ case value
88
+ when nil then '(empty)'
89
+ when '' then '(blank)'
90
+ when Time then format_time(value)
91
+ when Date then format_date(value)
92
+ else value.to_s.truncate(100)
93
+ end
94
+ end
95
+
96
+ def format_time(time)
97
+ time.strftime('%B %d, %Y at %I:%M %p')
98
+ end
99
+
100
+ def format_date(date)
101
+ date.strftime('%B %d, %Y')
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaperTrailHistory
4
+ # Service class for querying and managing PaperTrail version records
5
+ class VersionService
6
+ def self.for_model(model_name, params = {})
7
+ trackable_model = TrackableModel.find(model_name)
8
+ return PaperTrail::Version.none unless trackable_model
9
+
10
+ versions = base_versions_for_model(trackable_model)
11
+ apply_filters(versions, params).order(created_at: :desc)
12
+ end
13
+
14
+ def self.for_record(model_name, item_id, params = {})
15
+ trackable_model = TrackableModel.find(model_name)
16
+ return PaperTrail::Version.none unless trackable_model
17
+
18
+ versions = base_versions_for_record(trackable_model, item_id)
19
+ apply_record_filters(versions, params).order(created_at: :desc)
20
+ end
21
+
22
+ def self.restore_version(version_id)
23
+ version = find_version_across_tables(version_id)
24
+ return validate_version_for_restore(version) unless version_restorable?(version)
25
+
26
+ perform_version_restore(version)
27
+ rescue StandardError => e
28
+ { success: false, error: e.message }
29
+ end
30
+
31
+ def self.unique_whodunnits(model_name = nil)
32
+ if model_name
33
+ trackable_model = TrackableModel.find(model_name)
34
+ return [] unless trackable_model
35
+
36
+ scope = trackable_model.version_class.where(item_type: trackable_model.item_type_for_versions)
37
+ else
38
+ scope = all_version_classes.flat_map(&:all)
39
+ return scope.map(&:whodunnit).compact.uniq.sort
40
+ end
41
+
42
+ scope.distinct.pluck(:whodunnit).compact.sort
43
+ end
44
+
45
+ def self.available_events(model_name = nil)
46
+ if model_name
47
+ trackable_model = TrackableModel.find(model_name)
48
+ return [] unless trackable_model
49
+
50
+ scope = trackable_model.version_class.where(item_type: trackable_model.item_type_for_versions)
51
+ else
52
+ scope = all_version_classes.flat_map(&:all)
53
+ return scope.map(&:event).compact.uniq.sort
54
+ end
55
+
56
+ scope.distinct.pluck(:event).compact.sort
57
+ end
58
+
59
+ def self.find_version_across_tables(version_id)
60
+ # Try to find the version in all possible version classes
61
+ all_version_classes.each do |version_class|
62
+ version = version_class.find_by(id: version_id)
63
+ return version if version
64
+ end
65
+ nil
66
+ end
67
+
68
+ def self.all_version_classes
69
+ @all_version_classes ||= begin
70
+ classes = Set.new
71
+ TrackableModel.all.each do |trackable_model|
72
+ classes.add(trackable_model.version_class)
73
+ end
74
+ classes.to_a
75
+ end
76
+ end
77
+
78
+ def self.filter_by_event(versions, event)
79
+ versions.where(event: event)
80
+ end
81
+
82
+ def self.filter_by_whodunnit(versions, whodunnit)
83
+ versions.where(whodunnit: whodunnit)
84
+ end
85
+
86
+ def self.filter_by_date_range(versions, from_date, to_date)
87
+ versions = versions.where(created_at: Date.parse(from_date)..) if from_date.present?
88
+ versions = versions.where(created_at: ..Date.parse(to_date).end_of_day) if to_date.present?
89
+ versions
90
+ end
91
+
92
+ def self.search_object_changes(versions, search_term)
93
+ versions.where('object_changes LIKE ? OR object LIKE ?', "%#{search_term}%", "%#{search_term}%")
94
+ end
95
+
96
+ def self.base_versions_for_model(trackable_model)
97
+ trackable_model.version_class.where(item_type: trackable_model.item_type_for_versions)
98
+ end
99
+
100
+ def self.apply_filters(versions, params)
101
+ versions = filter_by_event(versions, params[:event]) if params[:event].present?
102
+ versions = filter_by_whodunnit(versions, params[:whodunnit]) if params[:whodunnit].present?
103
+ versions = apply_date_range_filter(versions, params) if date_range_params?(params)
104
+ versions = search_object_changes(versions, params[:search]) if params[:search].present?
105
+ versions
106
+ end
107
+
108
+ def self.apply_date_range_filter(versions, params)
109
+ filter_by_date_range(versions, params[:from_date], params[:to_date])
110
+ end
111
+
112
+ def self.date_range_params?(params)
113
+ params[:from_date].present? || params[:to_date].present?
114
+ end
115
+
116
+ def self.base_versions_for_record(trackable_model, item_id)
117
+ trackable_model.version_class.where(
118
+ item_type: trackable_model.item_type_for_versions,
119
+ item_id: item_id
120
+ )
121
+ end
122
+
123
+ def self.apply_record_filters(versions, params)
124
+ versions = filter_by_event(versions, params[:event]) if params[:event].present?
125
+ versions = apply_date_range_filter(versions, params) if date_range_params?(params)
126
+ versions
127
+ end
128
+
129
+ def self.version_restorable?(version)
130
+ version && version.event != 'create'
131
+ end
132
+
133
+ def self.validate_version_for_restore(version)
134
+ return { success: false, error: I18n.t('paper_trail_history.errors.version_not_found') } unless version
135
+
136
+ { success: false, error: I18n.t('paper_trail_history.errors.cannot_restore_create') }
137
+ end
138
+
139
+ def self.perform_version_restore(version)
140
+ if version.event == 'destroy'
141
+ restore_destroyed_record(version)
142
+ else
143
+ restore_previous_version(version)
144
+ end
145
+ end
146
+
147
+ def self.restore_destroyed_record(version)
148
+ restored_item = version.reify
149
+ restored_item.save!
150
+ { success: true, item: restored_item, message: I18n.t('paper_trail_history.messages.restore_from_deletion') }
151
+ end
152
+
153
+ def self.restore_previous_version(version)
154
+ version.reify.save!
155
+ { success: true, item: version.item, message: I18n.t('paper_trail_history.messages.restore_to_previous') }
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,64 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <title>Paper Trail History</title>
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <%= csrf_meta_tags %>
7
+ <%= csp_meta_tag %>
8
+
9
+ <%= yield :head %>
10
+
11
+ <%= stylesheet_link_tag "paper_trail_history/application", media: "all" %>
12
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
13
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
14
+ </head>
15
+ <body>
16
+ <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
17
+ <div class="container">
18
+ <%= link_to "Paper Trail History", root_path, class: "navbar-brand" %>
19
+
20
+ <div class="navbar-nav ms-auto">
21
+ <%= link_to "All Models", models_path, class: "nav-link" %>
22
+ </div>
23
+ </div>
24
+ </nav>
25
+
26
+ <div class="container mt-4">
27
+ <% if notice %>
28
+ <div class="alert alert-success alert-dismissible fade show" role="alert">
29
+ <%= notice %>
30
+ <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
31
+ </div>
32
+ <% end %>
33
+
34
+ <% if alert %>
35
+ <div class="alert alert-danger alert-dismissible fade show" role="alert">
36
+ <%= alert %>
37
+ <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
38
+ </div>
39
+ <% end %>
40
+
41
+ <%= yield %>
42
+ </div>
43
+
44
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
45
+ <script>
46
+ // Initialize Bootstrap tooltips - works with both regular apps and Turbo
47
+ function initTooltips() {
48
+ // Dispose of existing tooltips first to prevent duplicates
49
+ document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(el => {
50
+ const existingTooltip = bootstrap.Tooltip.getInstance(el);
51
+ if (existingTooltip) existingTooltip.dispose();
52
+ });
53
+
54
+ // Initialize new tooltips
55
+ const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
56
+ [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
57
+ }
58
+
59
+ // Initialize on both initial load and Turbo navigation
60
+ document.addEventListener('DOMContentLoaded', initTooltips);
61
+ document.addEventListener('turbo:load', initTooltips);
62
+ </script>
63
+ </body>
64
+ </html>
@@ -0,0 +1,65 @@
1
+ <div class="d-flex justify-content-between align-items-center mb-4">
2
+ <h1>Trackable Models</h1>
3
+ <span class="badge bg-secondary"><%= @trackable_models.count %> models</span>
4
+ </div>
5
+
6
+ <% if @trackable_models.any? %>
7
+ <div class="card">
8
+ <div class="card-body">
9
+ <div class="table-responsive">
10
+ <table class="table table-striped">
11
+ <thead>
12
+ <tr>
13
+ <th><i class="bi bi-database-fill"></i> Model</th>
14
+ <th>Table Name</th>
15
+ <th>Version Table</th>
16
+ <th>Total Versions</th>
17
+ <th>Actions</th>
18
+ </tr>
19
+ </thead>
20
+ <tbody>
21
+ <% @trackable_models.each do |model| %>
22
+ <tr>
23
+ <td>
24
+ <strong><%= model.human_name %></strong>
25
+ <br>
26
+ <small class="text-muted"><%= model.name %></small>
27
+ </td>
28
+ <td>
29
+ <code><%= model.table_name %></code>
30
+ </td>
31
+ <td>
32
+ <code><%= model.version_table_name %></code>
33
+ <br>
34
+ <small class="text-muted"><%= model.version_class.name %></small>
35
+ </td>
36
+ <td>
37
+ <span class="badge bg-primary"><%= model.total_versions_count %></span>
38
+ </td>
39
+ <td>
40
+ <div class="btn-group btn-group-sm" role="group">
41
+ <%= link_to model_path(model.name),
42
+ class: "btn btn-outline-secondary",
43
+ data: { bs_toggle: "tooltip", bs_title: "View Model Details" } do %>
44
+ <i class="bi bi-eye"></i>
45
+ <% end %>
46
+ <%= link_to versions_model_path(model.name),
47
+ class: "btn btn-primary",
48
+ data: { bs_toggle: "tooltip", bs_title: "View All Versions" } do %>
49
+ <i class="bi bi-clock-history"></i>
50
+ <% end %>
51
+ </div>
52
+ </td>
53
+ </tr>
54
+ <% end %>
55
+ </tbody>
56
+ </table>
57
+ </div>
58
+ </div>
59
+ </div>
60
+ <% else %>
61
+ <div class="alert alert-info">
62
+ <i class="bi bi-info-circle"></i>
63
+ No trackable models found. Make sure you have models with <code>has_paper_trail</code> configured.
64
+ </div>
65
+ <% end %>
@@ -0,0 +1,49 @@
1
+ <nav aria-label="breadcrumb">
2
+ <ol class="breadcrumb">
3
+ <li class="breadcrumb-item"><%= link_to "Models", models_path %></li>
4
+ <li class="breadcrumb-item active"><%= @trackable_model.human_name %></li>
5
+ </ol>
6
+ </nav>
7
+
8
+ <div class="d-flex justify-content-between align-items-center mb-4">
9
+ <h1><%= @trackable_model.human_name %></h1>
10
+ <div>
11
+ <%= link_to "All Versions", versions_model_path(@trackable_model.name), class: "btn btn-primary" %>
12
+ </div>
13
+ </div>
14
+
15
+ <div class="row mb-4">
16
+ <div class="col-md-8">
17
+ <div class="card">
18
+ <div class="card-body">
19
+ <h5 class="card-title">Model Information</h5>
20
+ <dl class="row">
21
+ <dt class="col-sm-3">Model Name:</dt>
22
+ <dd class="col-sm-9"><code><%= @trackable_model.name %></code></dd>
23
+
24
+ <dt class="col-sm-3">Table Name:</dt>
25
+ <dd class="col-sm-9"><code><%= @trackable_model.table_name %></code></dd>
26
+
27
+ <dt class="col-sm-3">Total Versions:</dt>
28
+ <dd class="col-sm-9">
29
+ <span class="badge bg-primary"><%= @trackable_model.total_versions_count %></span>
30
+ </dd>
31
+ </dl>
32
+ </div>
33
+ </div>
34
+ </div>
35
+ </div>
36
+
37
+ <div class="card">
38
+ <div class="card-header d-flex justify-content-between align-items-center">
39
+ <h5 class="mb-0">Recent Versions</h5>
40
+ <%= link_to "View All", versions_model_path(@trackable_model.name), class: "btn btn-sm btn-outline-primary" %>
41
+ </div>
42
+ <div class="card-body">
43
+ <% if @recent_versions.any? %>
44
+ <%= render "paper_trail_history/shared/versions_table", versions: @recent_versions, show_item_link: true %>
45
+ <% else %>
46
+ <p class="text-muted">No versions found for this model.</p>
47
+ <% end %>
48
+ </div>
49
+ </div>
@@ -0,0 +1,30 @@
1
+ <nav aria-label="breadcrumb">
2
+ <ol class="breadcrumb">
3
+ <li class="breadcrumb-item"><%= link_to "Models", models_path %></li>
4
+ <li class="breadcrumb-item"><%= link_to @trackable_model.human_name, model_path(@trackable_model.name) %></li>
5
+ <li class="breadcrumb-item active">All Versions</li>
6
+ </ol>
7
+ </nav>
8
+
9
+ <div class="d-flex justify-content-between align-items-center mb-4">
10
+ <h1>All Versions - <%= @trackable_model.human_name %></h1>
11
+ <span class="badge bg-secondary"><%= @versions.count %> versions</span>
12
+ </div>
13
+
14
+ <%= render "paper_trail_history/shared/version_filters",
15
+ available_events: @available_events,
16
+ available_whodunnits: @available_whodunnits %>
17
+
18
+ <div class="card">
19
+ <div class="card-body">
20
+ <% if @decorated_versions.any? %>
21
+ <%= render "paper_trail_history/shared/versions_table", versions: @decorated_versions, show_item_link: true %>
22
+ <% else %>
23
+ <div class="text-center py-4">
24
+ <i class="bi bi-inbox display-1 text-muted"></i>
25
+ <p class="text-muted">No versions found matching your criteria.</p>
26
+ <%= link_to "Clear Filters", versions_model_path(@trackable_model.name), class: "btn btn-outline-secondary" %>
27
+ </div>
28
+ <% end %>
29
+ </div>
30
+ </div>
@@ -0,0 +1,57 @@
1
+ <nav aria-label="breadcrumb">
2
+ <ol class="breadcrumb">
3
+ <li class="breadcrumb-item"><%= link_to "Models", models_path %></li>
4
+ <li class="breadcrumb-item"><%= link_to @trackable_model.human_name, model_path(@trackable_model.name) %></li>
5
+ <li class="breadcrumb-item active">Record #<%= @record_id %></li>
6
+ </ol>
7
+ </nav>
8
+
9
+ <div class="d-flex justify-content-between align-items-center mb-4">
10
+ <h1>
11
+ <%= @trackable_model.human_name %> #<%= @record_id %>
12
+ <% if @record.nil? %>
13
+ <span class="badge bg-danger">Deleted</span>
14
+ <% end %>
15
+ </h1>
16
+ <div>
17
+ <%= link_to "All Versions", versions_model_record_path(@trackable_model.name, @record_id), class: "btn btn-primary" %>
18
+ </div>
19
+ </div>
20
+
21
+ <% if @record %>
22
+ <div class="row mb-4">
23
+ <div class="col-md-8">
24
+ <div class="card">
25
+ <div class="card-body">
26
+ <h5 class="card-title">Current Record</h5>
27
+ <dl class="row">
28
+ <% @record.attributes.each do |attr, value| %>
29
+ <dt class="col-sm-3"><%= attr.humanize %>:</dt>
30
+ <dd class="col-sm-9">
31
+ <% if value.nil? %>
32
+ <em class="text-muted">(empty)</em>
33
+ <% else %>
34
+ <%= truncate(value.to_s, length: 100) %>
35
+ <% end %>
36
+ </dd>
37
+ <% end %>
38
+ </dl>
39
+ </div>
40
+ </div>
41
+ </div>
42
+ </div>
43
+ <% end %>
44
+
45
+ <div class="card">
46
+ <div class="card-header d-flex justify-content-between align-items-center">
47
+ <h5 class="mb-0">Version History</h5>
48
+ <%= link_to "View All", versions_model_record_path(@trackable_model.name, @record_id), class: "btn btn-sm btn-outline-primary" %>
49
+ </div>
50
+ <div class="card-body">
51
+ <% if @recent_versions.any? %>
52
+ <%= render "paper_trail_history/shared/versions_table", versions: @recent_versions %>
53
+ <% else %>
54
+ <p class="text-muted">No versions found for this record.</p>
55
+ <% end %>
56
+ </div>
57
+ </div>
@@ -0,0 +1,36 @@
1
+ <nav aria-label="breadcrumb">
2
+ <ol class="breadcrumb">
3
+ <li class="breadcrumb-item"><%= link_to "Models", models_path %></li>
4
+ <li class="breadcrumb-item"><%= link_to @trackable_model.human_name, model_path(@trackable_model.name) %></li>
5
+ <li class="breadcrumb-item"><%= link_to "Record ##{@record_id}", model_record_path(@trackable_model.name, @record_id) %></li>
6
+ <li class="breadcrumb-item active">All Versions</li>
7
+ </ol>
8
+ </nav>
9
+
10
+ <div class="d-flex justify-content-between align-items-center mb-4">
11
+ <h1>
12
+ All Versions - <%= @trackable_model.human_name %> #<%= @record_id %>
13
+ <% if @record.nil? %>
14
+ <span class="badge bg-danger">Deleted</span>
15
+ <% end %>
16
+ </h1>
17
+ <span class="badge bg-secondary"><%= @versions.count %> versions</span>
18
+ </div>
19
+
20
+ <%= render "paper_trail_history/shared/version_filters",
21
+ available_events: @available_events,
22
+ available_whodunnits: nil %>
23
+
24
+ <div class="card">
25
+ <div class="card-body">
26
+ <% if @decorated_versions.any? %>
27
+ <%= render "paper_trail_history/shared/versions_table", versions: @decorated_versions %>
28
+ <% else %>
29
+ <div class="text-center py-4">
30
+ <i class="bi bi-inbox display-1 text-muted"></i>
31
+ <p class="text-muted">No versions found matching your criteria.</p>
32
+ <%= link_to "Clear Filters", versions_model_record_path(@trackable_model.name, @record_id), class: "btn btn-outline-secondary" %>
33
+ </div>
34
+ <% end %>
35
+ </div>
36
+ </div>
@@ -0,0 +1,54 @@
1
+ <div class="card mb-4">
2
+ <div class="card-header">
3
+ <h6 class="mb-0">
4
+ <i class="bi bi-funnel"></i>
5
+ Filters
6
+ <button class="btn btn-sm btn-outline-secondary ms-2" type="button" data-bs-toggle="collapse" data-bs-target="#filterCollapse">
7
+ Toggle
8
+ </button>
9
+ </h6>
10
+ </div>
11
+ <div class="collapse" id="filterCollapse">
12
+ <div class="card-body">
13
+ <%= form_with url: request.path, method: :get, local: true, class: "row g-3" do |f| %>
14
+ <div class="col-md-3">
15
+ <%= f.label :event, class: "form-label" %>
16
+ <%= f.select :event,
17
+ options_for_select([["All Events", ""]] + available_events.map { |e| [e.humanize, e] }, params[:event]),
18
+ {}, { class: "form-select" } %>
19
+ </div>
20
+
21
+ <% if available_whodunnits %>
22
+ <div class="col-md-3">
23
+ <%= f.label :whodunnit, "Who", class: "form-label" %>
24
+ <%= f.select :whodunnit,
25
+ options_for_select([["Anyone", ""]] + available_whodunnits.map { |w| [w || "System", w] }, params[:whodunnit]),
26
+ {}, { class: "form-select" } %>
27
+ </div>
28
+ <% end %>
29
+
30
+ <div class="col-md-2">
31
+ <%= f.label :from_date, "From Date", class: "form-label" %>
32
+ <%= f.date_field :from_date, value: params[:from_date], class: "form-control" %>
33
+ </div>
34
+
35
+ <div class="col-md-2">
36
+ <%= f.label :to_date, "To Date", class: "form-label" %>
37
+ <%= f.date_field :to_date, value: params[:to_date], class: "form-control" %>
38
+ </div>
39
+
40
+ <div class="col-md-2">
41
+ <%= f.label :search, "Search Changes", class: "form-label" %>
42
+ <%= f.text_field :search, value: params[:search], class: "form-control", placeholder: "Search..." %>
43
+ </div>
44
+
45
+ <div class="col-12">
46
+ <div class="d-flex gap-2">
47
+ <%= f.submit "Apply Filters", class: "btn btn-primary" %>
48
+ <%= link_to "Clear", request.path, class: "btn btn-outline-secondary" %>
49
+ </div>
50
+ </div>
51
+ <% end %>
52
+ </div>
53
+ </div>
54
+ </div>
@@ -0,0 +1,60 @@
1
+ <div class="table-responsive">
2
+ <table class="table table-striped">
3
+ <thead>
4
+ <tr>
5
+ <th>Version</th>
6
+ <th>Event</th>
7
+ <th>When</th>
8
+ <th>Who</th>
9
+ <% if local_assigns[:show_item_link] %>
10
+ <th>Item</th>
11
+ <% end %>
12
+ <th>Actions</th>
13
+ </tr>
14
+ </thead>
15
+ <tbody>
16
+ <% versions.each do |decorated_version| %>
17
+ <tr>
18
+ <td>
19
+ <%= link_to "##{decorated_version.version.id}",
20
+ version_path(decorated_version.version.id),
21
+ class: "text-decoration-none" %>
22
+ </td>
23
+ <td>
24
+ <span class="badge bg-<%= decorated_version.event_class %>">
25
+ <i class="bi bi-<%= decorated_version.event == 'create' ? 'plus' : decorated_version.event == 'update' ? 'pencil' : 'trash' %>"></i>
26
+ <%= decorated_version.event_label %>
27
+ </span>
28
+ </td>
29
+ <td>
30
+ <small><%= decorated_version.formatted_created_at %></small>
31
+ </td>
32
+ <td>
33
+ <small><%= decorated_version.whodunnit_display %></small>
34
+ </td>
35
+ <% if local_assigns[:show_item_link] %>
36
+ <td>
37
+ <%= link_to decorated_version.item_display_name,
38
+ model_record_path(decorated_version.version.item_type, decorated_version.version.item_id),
39
+ class: "text-decoration-none" %>
40
+ </td>
41
+ <% end %>
42
+ <td>
43
+ <div class="btn-group btn-group-sm">
44
+ <%= link_to "View", version_path(decorated_version.version.id), class: "btn btn-outline-primary btn-sm" %>
45
+ <% if decorated_version.can_restore? %>
46
+ <%= link_to "Restore",
47
+ restore_version_path(decorated_version.version.id),
48
+ method: :patch,
49
+ data: {
50
+ confirm: "Are you sure you want to restore this version?"
51
+ },
52
+ class: "btn btn-outline-warning btn-sm" %>
53
+ <% end %>
54
+ </div>
55
+ </td>
56
+ </tr>
57
+ <% end %>
58
+ </tbody>
59
+ </table>
60
+ </div>