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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +188 -0
- data/Rakefile +29 -0
- data/app/assets/stylesheets/paper_trail_history/application.css +93 -0
- data/app/controllers/paper_trail_history/application_controller.rb +6 -0
- data/app/controllers/paper_trail_history/models_controller.rb +57 -0
- data/app/controllers/paper_trail_history/records_controller.rb +58 -0
- data/app/controllers/paper_trail_history/versions_controller.rb +33 -0
- data/app/helpers/paper_trail_history/application_helper.rb +7 -0
- data/app/jobs/paper_trail_history/application_job.rb +6 -0
- data/app/mailers/paper_trail_history/application_mailer.rb +9 -0
- data/app/models/paper_trail_history/application_record.rb +8 -0
- data/app/models/paper_trail_history/trackable_model.rb +112 -0
- data/app/models/paper_trail_history/version_decorator.rb +104 -0
- data/app/models/paper_trail_history/version_service.rb +158 -0
- data/app/views/layouts/paper_trail_history/application.html.erb +64 -0
- data/app/views/paper_trail_history/models/index.html.erb +65 -0
- data/app/views/paper_trail_history/models/show.html.erb +49 -0
- data/app/views/paper_trail_history/models/versions.html.erb +30 -0
- data/app/views/paper_trail_history/records/show.html.erb +57 -0
- data/app/views/paper_trail_history/records/versions.html.erb +36 -0
- data/app/views/paper_trail_history/shared/_version_filters.html.erb +54 -0
- data/app/views/paper_trail_history/shared/_versions_table.html.erb +60 -0
- data/app/views/paper_trail_history/versions/show.html.erb +134 -0
- data/config/locales/de.yml +16 -0
- data/config/locales/en.yml +16 -0
- data/config/routes.rb +23 -0
- data/lib/paper_trail_history/engine.rb +8 -0
- data/lib/paper_trail_history/version.rb +5 -0
- data/lib/paper_trail_history.rb +15 -0
- data/lib/tasks/paper_trail_history_tasks.rake +6 -0
- 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>
|