paper_view 0.1.0 → 0.1.1
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/README.md +12 -2
- data/app/models/paper_view/timeline.rb +2 -2
- data/app/models/paper_view/version_row.rb +23 -5
- data/app/views/layouts/paper_view/application.html.erb +1 -2
- data/app/views/paper_view/shared/_script.html.erb +2 -0
- data/app/views/paper_view/shared/_styles.html.erb +19 -12
- data/app/views/paper_view/versions/_panel.html.erb +52 -40
- data/app/views/paper_view/versions/_timeline_row.html.erb +6 -6
- data/app/views/paper_view/versions/index.html.erb +3 -3
- data/lib/paper_view/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 83879cfa36b7725f33b545fff5fe55fb4903b8a787f279e84bb90493ba282955
|
|
4
|
+
data.tar.gz: 44bcabbe8a90f4c9c162ea78531677cec55855f1d89421fb8b930f1b2ee384d4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dc43978efac30d7baad54ed0b092ef53f4c61f7ed5c5cc854b2a2e65ce833739f7950d610ea02f3da3b526307d01f942fcf16dbed8b16aefbeac094151cb6f4c
|
|
7
|
+
data.tar.gz: ffd83f028d936b9f24c6ee8855d142249fd2be76f75bc68f6e98cf8afd141eab5686759e58def65be00d9c744ac26b72ce589c68c21525709c3a4095dc5c70a2
|
data/README.md
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# PaperView
|
|
2
2
|
|
|
3
3
|
A lightweight, mountable dashboard for [paper_trail](https://github.com/paper-trail-gem/paper_trail).
|
|
4
|
-
Browse the `versions` table and read, in
|
|
4
|
+
Browse the `versions` table and read, in a chronological timeline, what actually changed.
|
|
5
|
+
|
|
6
|
+

|
|
5
7
|
|
|
6
8
|
## Installation
|
|
7
9
|
|
|
@@ -18,7 +20,7 @@ gem "paper_view"
|
|
|
18
20
|
# config/routes.rb
|
|
19
21
|
Rails.application.routes.draw do
|
|
20
22
|
authenticate :user, ->(user) { user.admin? } do
|
|
21
|
-
mount PaperView::Engine => "/
|
|
23
|
+
mount PaperView::Engine => "/versions"
|
|
22
24
|
end
|
|
23
25
|
end
|
|
24
26
|
```
|
|
@@ -52,3 +54,11 @@ end
|
|
|
52
54
|
```
|
|
53
55
|
|
|
54
56
|
Visit `/paper_view`.
|
|
57
|
+
|
|
58
|
+
## PaperTrail setup
|
|
59
|
+
|
|
60
|
+
To work properly, PaperView requires an `object_changes` column in your `versions` table. This can be added by running the installation generator with the `--with-changes` option. This can be done even if you already have a `versions` table; it will generate a migration that adds the `object_changes` column to your existing `versions` table:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
rails generate paper_trail:install --with-changes
|
|
64
|
+
```
|
|
@@ -23,12 +23,12 @@ module PaperView
|
|
|
23
23
|
@items ||= rows.map(&:item_key).uniq
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
def
|
|
26
|
+
def multi_item_view?
|
|
27
27
|
items.size != 1
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
def title
|
|
31
|
-
|
|
31
|
+
multi_item_view? ? "All changes" : rows.first.item_label
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
def selected
|
|
@@ -42,17 +42,37 @@ module PaperView
|
|
|
42
42
|
"#{burst_size} versions within 1s"
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
+
def created?
|
|
46
|
+
event == "create"
|
|
47
|
+
end
|
|
48
|
+
|
|
45
49
|
def destroyed?
|
|
46
50
|
event == "destroy"
|
|
47
51
|
end
|
|
48
52
|
|
|
53
|
+
def snapshot?
|
|
54
|
+
created? || destroyed?
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def change_label
|
|
58
|
+
noun = case event
|
|
59
|
+
when "create" then "initial value"
|
|
60
|
+
when "destroy" then "final value"
|
|
61
|
+
else "changed field"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
"#{change_set.size} #{noun.pluralize(change_set.size)}"
|
|
65
|
+
end
|
|
66
|
+
|
|
49
67
|
def lines
|
|
50
|
-
leaves.first(MAX_LINES)
|
|
68
|
+
snapshot? ? [] : leaves.first(MAX_LINES)
|
|
51
69
|
end
|
|
52
70
|
|
|
53
71
|
def footnotes
|
|
54
72
|
notes = []
|
|
55
73
|
notes << "payload not deserializable" if change_set.unreadable?
|
|
74
|
+
return notes if snapshot?
|
|
75
|
+
|
|
56
76
|
notes << "..." if overflow_count.positive?
|
|
57
77
|
notes << "no attribute changes recorded" if leaves.empty? && !change_set.unreadable?
|
|
58
78
|
notes
|
|
@@ -81,14 +101,12 @@ module PaperView
|
|
|
81
101
|
version = #{PaperView.config.version_class_name}.find(#{id})
|
|
82
102
|
item = version.reify # the deleted record
|
|
83
103
|
|
|
84
|
-
item.
|
|
85
|
-
|
|
86
|
-
# item.save! # re-create the record
|
|
104
|
+
# item.save! # re-create the record; columns added since are nil
|
|
87
105
|
RUBY
|
|
88
106
|
end
|
|
89
107
|
|
|
90
108
|
def leaves
|
|
91
|
-
@leaves ||=
|
|
109
|
+
@leaves ||= change_set.flat_map(&:leaves)
|
|
92
110
|
end
|
|
93
111
|
|
|
94
112
|
def overflow_count
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<meta name="robots" content="noindex, nofollow">
|
|
9
9
|
|
|
10
10
|
<title>PaperView</title>
|
|
11
|
-
<%= tag.link rel: "icon", href: 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="10 0 100 100"><text y=".90em" font-size="90"
|
|
11
|
+
<%= tag.link rel: "icon", href: 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="10 0 100 100"><text y=".90em" font-size="90">📝</text></svg>' %>
|
|
12
12
|
<%= csrf_meta_tags %>
|
|
13
13
|
<%= csp_meta_tag %>
|
|
14
14
|
<%= render "paper_view/shared/styles" %>
|
|
@@ -17,7 +17,6 @@
|
|
|
17
17
|
<body>
|
|
18
18
|
<header class="pv-header">
|
|
19
19
|
<%= link_to root_path, class: "pv-header__brand" do %>
|
|
20
|
-
<span class="pv-header__dot"></span>
|
|
21
20
|
PaperView
|
|
22
21
|
<% end %>
|
|
23
22
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<style<%= paper_view_nonce_attribute %>>
|
|
1
|
+
<style <%= paper_view_nonce_attribute %>>
|
|
2
2
|
*, *::before, *::after { box-sizing: border-box; }
|
|
3
3
|
[hidden] { display: none !important; }
|
|
4
4
|
|
|
@@ -94,7 +94,6 @@
|
|
|
94
94
|
}
|
|
95
95
|
.pv-header__brand { display: flex; align-items: baseline; gap: 8px; color: var(--pv-ink); font-size: 15px; font-weight: 650; }
|
|
96
96
|
.pv-header__brand:hover { color: var(--pv-ink); text-decoration: none; }
|
|
97
|
-
.pv-header__dot { width: 8px; height: 8px; border-radius: 50%; background: var(--pv-accent); }
|
|
98
97
|
.pv-header__meta { margin-left: auto; color: var(--pv-ink-soft); font-size: 12.5px; }
|
|
99
98
|
|
|
100
99
|
.pv-main { max-width: 1560px; margin: 0 auto; padding: 24px; }
|
|
@@ -222,7 +221,7 @@
|
|
|
222
221
|
border-bottom: 1px solid var(--pv-border-subtle);
|
|
223
222
|
cursor: pointer;
|
|
224
223
|
}
|
|
225
|
-
.pv-version--
|
|
224
|
+
.pv-version--multi-item-view { grid-template-columns: 64px 22px 168px 1fr auto; }
|
|
226
225
|
.pv-version:hover { background: var(--pv-row-hover); }
|
|
227
226
|
.pv-version--selected, .pv-version--selected:hover { background: var(--pv-row-selected); }
|
|
228
227
|
.pv-version:focus-visible { outline: 2px solid var(--pv-accent); outline-offset: -2px; }
|
|
@@ -242,7 +241,7 @@
|
|
|
242
241
|
.pv-version__content { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
|
|
243
242
|
.pv-version__meta { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
244
243
|
.pv-version__actor { font: 600 13px/20px var(--pv-sans); color: var(--pv-ink); }
|
|
245
|
-
.pv-version--
|
|
244
|
+
.pv-version--multi-item-view .pv-version__actor { font-weight: 400; color: var(--pv-ink-secondary); }
|
|
246
245
|
.pv-version__note { font: 400 12px/18px var(--pv-sans); color: var(--pv-ink-soft); }
|
|
247
246
|
|
|
248
247
|
.pv-pill { font: 500 11px/16px var(--pv-sans); border-radius: 4px; padding: 1px 6px; white-space: nowrap; }
|
|
@@ -262,19 +261,27 @@
|
|
|
262
261
|
text-overflow: ellipsis;
|
|
263
262
|
white-space: nowrap;
|
|
264
263
|
}
|
|
265
|
-
.pv-version--
|
|
264
|
+
.pv-version--multi-item-view .pv-value { max-width: 240px; }
|
|
266
265
|
.pv-value--old { color: var(--pv-del); background: var(--pv-del-bg); }
|
|
267
266
|
.pv-value--new { color: var(--pv-add); background: var(--pv-add-bg); }
|
|
268
267
|
|
|
269
|
-
.pv-side {
|
|
270
|
-
|
|
268
|
+
.pv-side {
|
|
269
|
+
position: sticky;
|
|
270
|
+
top: calc(var(--pv-header-h) + 16px);
|
|
271
|
+
width: clamp(330px, 26vw, 420px);
|
|
272
|
+
flex: none;
|
|
273
|
+
max-height: calc(100vh - var(--pv-header-h) - 32px);
|
|
274
|
+
overflow-y: auto;
|
|
275
|
+
overscroll-behavior: contain;
|
|
276
|
+
}
|
|
277
|
+
.pv-detail__head { display: flex; align-items: baseline; justify-content: space-between; gap: 4px 12px; flex-wrap: wrap; padding: 14px 16px; border-bottom: 1px solid var(--pv-border); }
|
|
271
278
|
.pv-detail__id { font: 600 13px/1.2 var(--pv-mono); color: var(--pv-ink); }
|
|
272
279
|
.pv-detail__stamp { font: 400 12px/1.2 var(--pv-sans); color: var(--pv-ink-soft); }
|
|
273
280
|
.pv-detail__meta { display: grid; gap: 12px; padding: 14px 16px; border-bottom: 1px solid var(--pv-border); }
|
|
274
|
-
.pv-detail__meta--split { grid-template-columns:
|
|
281
|
+
.pv-detail__meta--split { grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); }
|
|
275
282
|
.pv-detail__field { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
|
|
276
283
|
.pv-detail__item { font: 400 13px/1.4 var(--pv-mono); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
277
|
-
.pv-detail__who { font: 400 13px/1.4 var(--pv-sans); color: var(--pv-ink); }
|
|
284
|
+
.pv-detail__who { font: 400 13px/1.4 var(--pv-sans); color: var(--pv-ink); overflow-wrap: anywhere; }
|
|
278
285
|
.pv-detail__body { display: flex; flex-direction: column; gap: 14px; padding: 14px 16px; }
|
|
279
286
|
.pv-detail__note { margin: 0; font: 400 12px/1.6 var(--pv-sans); color: var(--pv-ink-soft); }
|
|
280
287
|
.pv-label { font: 600 11px/1.2 var(--pv-sans); letter-spacing: .06em; text-transform: uppercase; color: var(--pv-ink-faint); }
|
|
@@ -369,7 +376,7 @@
|
|
|
369
376
|
|
|
370
377
|
@media (max-width: 900px) {
|
|
371
378
|
.pv-layout { flex-direction: column; }
|
|
372
|
-
.pv-side { position: static; width: 100%; }
|
|
379
|
+
.pv-side { position: static; width: 100%; max-height: none; overflow: visible; }
|
|
373
380
|
}
|
|
374
381
|
|
|
375
382
|
@media (max-width: 720px) {
|
|
@@ -380,9 +387,9 @@
|
|
|
380
387
|
}
|
|
381
388
|
|
|
382
389
|
@media (max-width: 640px) {
|
|
383
|
-
.pv-version, .pv-version--
|
|
390
|
+
.pv-version, .pv-version--multi-item-view { grid-template-columns: 52px 16px 1fr; padding: 12px 14px; }
|
|
384
391
|
.pv-version__rail { grid-row: 1 / -1; }
|
|
385
392
|
.pv-version__item, .pv-version__content, .pv-version__id { grid-column: 3; }
|
|
386
|
-
.pv-value, .pv-version--
|
|
393
|
+
.pv-value, .pv-version--multi-item-view .pv-value { max-width: 100%; }
|
|
387
394
|
}
|
|
388
395
|
</style>
|
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
<span class="pv-detail__stamp"><%= paper_view_time(row.version.created_at) %></span>
|
|
9
9
|
</div>
|
|
10
10
|
|
|
11
|
-
<div class="pv-detail__meta<%= " pv-detail__meta--split" if
|
|
12
|
-
<% if
|
|
11
|
+
<div class="pv-detail__meta<%= " pv-detail__meta--split" if multi_item_view %>">
|
|
12
|
+
<% if multi_item_view %>
|
|
13
13
|
<div class="pv-detail__field">
|
|
14
14
|
<span class="pv-label">Item</span>
|
|
15
|
-
<%= link_to row.item_label, paper_view_item_path(row.version), class: "pv-detail__item" %>
|
|
15
|
+
<%= link_to row.item_label, paper_view_item_path(row.version), class: "pv-detail__item", title: row.item_label %>
|
|
16
16
|
</div>
|
|
17
17
|
<% end %>
|
|
18
18
|
|
|
@@ -26,11 +26,13 @@
|
|
|
26
26
|
<% if row.change_set.unreadable? %>
|
|
27
27
|
<p class="pv-detail__note">
|
|
28
28
|
The <code>object_changes</code> payload is neither readable YAML nor
|
|
29
|
-
JSON, so it cannot be diffed.
|
|
30
|
-
|
|
29
|
+
JSON, so it cannot be diffed.
|
|
30
|
+
<% unless row.created? %>
|
|
31
|
+
The stored bytes are below under <strong>Raw object_changes</strong>.
|
|
32
|
+
<% end %>
|
|
31
33
|
</p>
|
|
32
34
|
<% else %>
|
|
33
|
-
<span class="pv-label"><%= row.
|
|
35
|
+
<span class="pv-label"><%= row.change_label %></span>
|
|
34
36
|
|
|
35
37
|
<% if row.change_set.empty? %>
|
|
36
38
|
<p class="pv-detail__note">
|
|
@@ -42,7 +44,15 @@
|
|
|
42
44
|
<div class="pv-attribute">
|
|
43
45
|
<span class="pv-attribute__name"><%= change.name %></span>
|
|
44
46
|
|
|
45
|
-
<% if
|
|
47
|
+
<% if row.created? %>
|
|
48
|
+
<div class="pv-bars">
|
|
49
|
+
<div class="pv-bar pv-bar--new"><%= change.new_text %></div>
|
|
50
|
+
</div>
|
|
51
|
+
<% elsif row.destroyed? %>
|
|
52
|
+
<div class="pv-bars">
|
|
53
|
+
<div class="pv-bar pv-bar--old"><%= change.old_text %></div>
|
|
54
|
+
</div>
|
|
55
|
+
<% elsif change.nested? %>
|
|
46
56
|
<% change.leaf_changes.each do |leaf| %>
|
|
47
57
|
<div class="pv-keys">
|
|
48
58
|
<span class="pv-keys__name"><%= leaf.path %></span>
|
|
@@ -83,44 +93,46 @@
|
|
|
83
93
|
<% end %>
|
|
84
94
|
<% end %>
|
|
85
95
|
|
|
86
|
-
|
|
87
|
-
<
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
+
<% unless row.created? %>
|
|
97
|
+
<div class="pv-disclosures">
|
|
98
|
+
<button
|
|
99
|
+
type="button"
|
|
100
|
+
class="pv-disclosure"
|
|
101
|
+
aria-expanded="false"
|
|
102
|
+
aria-controls="pv-raw-<%= row.id %>"
|
|
103
|
+
data-pv-toggle="pv-raw-<%= row.id %>"
|
|
104
|
+
>
|
|
105
|
+
<span class="pv-disclosure__arrow">▸</span> Raw object_changes
|
|
106
|
+
</button>
|
|
96
107
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
108
|
+
<button
|
|
109
|
+
type="button"
|
|
110
|
+
class="pv-disclosure pv-disclosure--end"
|
|
111
|
+
aria-expanded="false"
|
|
112
|
+
aria-controls="pv-reify-<%= row.id %>"
|
|
113
|
+
data-pv-toggle="pv-reify-<%= row.id %>"
|
|
114
|
+
>
|
|
115
|
+
<span class="pv-disclosure__arrow">▸</span> Reify
|
|
116
|
+
</button>
|
|
106
117
|
|
|
107
|
-
|
|
118
|
+
<pre class="pv-raw" id="pv-raw-<%= row.id %>" hidden><%= row.change_set.raw_text %></pre>
|
|
108
119
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
120
|
+
<div class="pv-snippet" id="pv-reify-<%= row.id %>" hidden>
|
|
121
|
+
<div class="pv-snippet__head">
|
|
122
|
+
<span class="pv-snippet__label">Rails console</span>
|
|
112
123
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
124
|
+
<button
|
|
125
|
+
type="button"
|
|
126
|
+
class="pv-snippet__copy"
|
|
127
|
+
data-pv-copy="pv-snippet-<%= row.id %>"
|
|
128
|
+
>
|
|
129
|
+
Copy
|
|
130
|
+
</button>
|
|
131
|
+
</div>
|
|
121
132
|
|
|
122
|
-
|
|
133
|
+
<pre class="pv-snippet__code" id="pv-snippet-<%= row.id %>"><%= row.console_snippet %></pre>
|
|
134
|
+
</div>
|
|
123
135
|
</div>
|
|
124
|
-
|
|
136
|
+
<% end %>
|
|
125
137
|
</div>
|
|
126
138
|
</section>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<div
|
|
2
|
-
class="pv-version<%= " pv-version--selected" if selected %><%= " pv-version--
|
|
2
|
+
class="pv-version<%= " pv-version--selected" if selected %><%= " pv-version--multi-item-view" if multi_item_view %>"
|
|
3
3
|
data-pv-version="<%= row.id %>"
|
|
4
4
|
role="button"
|
|
5
5
|
tabindex="0"
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
<span class="pv-version__line"></span>
|
|
13
13
|
</div>
|
|
14
14
|
|
|
15
|
-
<% if
|
|
15
|
+
<% if multi_item_view %>
|
|
16
16
|
<div class="pv-version__item">
|
|
17
17
|
<%= link_to row.item_label, paper_view_item_path(row.version), class: "pv-version__item-link" %>
|
|
18
18
|
<span class="pv-pill pv-pill--<%= row.event %>"><%= row.event %></span>
|
|
@@ -23,12 +23,12 @@
|
|
|
23
23
|
<div class="pv-version__meta">
|
|
24
24
|
<span class="pv-version__actor"><%= paper_view_whodunnit(row.version) %></span>
|
|
25
25
|
|
|
26
|
-
<%
|
|
26
|
+
<% if !multi_item_view %>
|
|
27
27
|
<span class="pv-pill pv-pill--event"><%= row.event %></span>
|
|
28
|
-
<% end %>
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
<% if row.burst? %>
|
|
30
|
+
<span class="pv-pill pv-pill--burst"><%= row.burst_label %></span>
|
|
31
|
+
<% end %>
|
|
32
32
|
<% end %>
|
|
33
33
|
</div>
|
|
34
34
|
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
|
|
32
32
|
<span class="pv-card__meta">
|
|
33
33
|
<%= number_with_delimiter(@paginator.total_count) %>
|
|
34
|
-
<%= "version".pluralize(@paginator.total_count) %><%= " · #{@timeline.items.size} items" if @timeline.
|
|
34
|
+
<%= "version".pluralize(@paginator.total_count) %><%= " · #{@timeline.items.size} items" if @timeline.multi_item_view? %>
|
|
35
35
|
</span>
|
|
36
36
|
|
|
37
37
|
<span class="pv-spacer"></span>
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
<div class="pv-timeline__day"><%= group.label %></div>
|
|
59
59
|
|
|
60
60
|
<% group.rows.each do |row| %>
|
|
61
|
-
<%= render "paper_view/versions/timeline_row", row: row,
|
|
61
|
+
<%= render "paper_view/versions/timeline_row", row: row, multi_item_view: @timeline.multi_item_view?, selected: row == @timeline.selected %>
|
|
62
62
|
<% end %>
|
|
63
63
|
</div>
|
|
64
64
|
<% end %>
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
|
|
70
70
|
<aside class="pv-side" data-pv-panels>
|
|
71
71
|
<% @timeline.rows.each do |row| %>
|
|
72
|
-
<%= render "paper_view/versions/panel", row: row,
|
|
72
|
+
<%= render "paper_view/versions/panel", row: row, multi_item_view: @timeline.multi_item_view?, active: row == @timeline.selected %>
|
|
73
73
|
<% end %>
|
|
74
74
|
</aside>
|
|
75
75
|
</div>
|
data/lib/paper_view/version.rb
CHANGED