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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0b12547320bb6d8dd78354092beee8c80f6fe2936e7476ef766791c7fdd2caa2
4
- data.tar.gz: 35aef5e07fbf35d80559c57e961ad7b388aaee2de8ca7d16e0f6c8279f48d096
3
+ metadata.gz: 83879cfa36b7725f33b545fff5fe55fb4903b8a787f279e84bb90493ba282955
4
+ data.tar.gz: 44bcabbe8a90f4c9c162ea78531677cec55855f1d89421fb8b930f1b2ee384d4
5
5
  SHA512:
6
- metadata.gz: 5771770579c64ff7442fbbfdea10a9b071917e45c2e0ecda1cd8fa8e30d8c461eaa21ffa85bf81fa389a9e507ac7bebcf831cdc3a41990224aa69ae45eb201ef
7
- data.tar.gz: e4129deda604f38f7c862ebf8afa45e1b4b2bc18930ed747f647170e76b3dda48080562940041990939bc056e67b3bf8400d71cc7cac2325710dadbebfe75f45
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 one chronological timeline, what actually changed.
4
+ Browse the `versions` table and read, in a chronological timeline, what actually changed.
5
+
6
+ ![preview](https://res.cloudinary.com/dlvuixik3/image/upload/v1789159753/paper_view_preview_aclugn.png)
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 => "/paper_view"
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 stream?
26
+ def multi_item_view?
27
27
  items.size != 1
28
28
  end
29
29
 
30
30
  def title
31
- stream? ? "All changes" : rows.first.item_label
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.attributes # columns added since are nil here
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 ||= destroyed? ? [LeafChange.new("record", item_label, nil)] : change_set.flat_map(&: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">👀</text></svg>' %>
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
 
@@ -54,6 +54,8 @@
54
54
  if (panel.hidden) collapse(panel);
55
55
  });
56
56
 
57
+ panels.scrollTop = 0;
58
+
57
59
  var url = new URL(window.location.href);
58
60
  url.searchParams.set("version", id);
59
61
  window.history.replaceState({}, "", url);
@@ -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--stream { grid-template-columns: 64px 22px 168px 1fr auto; }
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--stream .pv-version__actor { font-weight: 400; color: var(--pv-ink-secondary); }
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--stream .pv-value { max-width: 240px; }
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 { position: sticky; top: calc(var(--pv-header-h) + 16px); width: 330px; flex: none; }
270
- .pv-detail__head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 14px 16px; border-bottom: 1px solid var(--pv-border); }
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: 1fr 1fr; }
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--stream { grid-template-columns: 52px 16px 1fr; padding: 12px 14px; }
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--stream .pv-value { max-width: 100%; }
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 stream %>">
12
- <% if stream %>
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. The stored bytes are below under
30
- <strong>Raw object_changes</strong>.
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.change_set.size %> changed <%= "field".pluralize(row.change_set.size) %></span>
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 change.nested? %>
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
- <div class="pv-disclosures">
87
- <button
88
- type="button"
89
- class="pv-disclosure"
90
- aria-expanded="false"
91
- aria-controls="pv-raw-<%= row.id %>"
92
- data-pv-toggle="pv-raw-<%= row.id %>"
93
- >
94
- <span class="pv-disclosure__arrow">▸</span> Raw object_changes
95
- </button>
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
- <button
98
- type="button"
99
- class="pv-disclosure pv-disclosure--end"
100
- aria-expanded="false"
101
- aria-controls="pv-reify-<%= row.id %>"
102
- data-pv-toggle="pv-reify-<%= row.id %>"
103
- >
104
- <span class="pv-disclosure__arrow">▸</span> Reify
105
- </button>
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
- <pre class="pv-raw" id="pv-raw-<%= row.id %>" hidden><%= row.change_set.raw_text %></pre>
118
+ <pre class="pv-raw" id="pv-raw-<%= row.id %>" hidden><%= row.change_set.raw_text %></pre>
108
119
 
109
- <div class="pv-snippet" id="pv-reify-<%= row.id %>" hidden>
110
- <div class="pv-snippet__head">
111
- <span class="pv-snippet__label">Rails console</span>
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
- <button
114
- type="button"
115
- class="pv-snippet__copy"
116
- data-pv-copy="pv-snippet-<%= row.id %>"
117
- >
118
- Copy
119
- </button>
120
- </div>
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
- <pre class="pv-snippet__code" id="pv-snippet-<%= row.id %>"><%= row.console_snippet %></pre>
133
+ <pre class="pv-snippet__code" id="pv-snippet-<%= row.id %>"><%= row.console_snippet %></pre>
134
+ </div>
123
135
  </div>
124
- </div>
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--stream" if stream %>"
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 stream %>
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
- <% unless stream %>
26
+ <% if !multi_item_view %>
27
27
  <span class="pv-pill pv-pill--event"><%= row.event %></span>
28
- <% end %>
29
28
 
30
- <% if row.burst? %>
31
- <span class="pv-pill pv-pill--burst"><%= row.burst_label %></span>
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.stream? %>
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, stream: @timeline.stream?, selected: row == @timeline.selected %>
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, stream: @timeline.stream?, active: row == @timeline.selected %>
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>
@@ -1,3 +1,3 @@
1
1
  module PaperView
2
- VERSION = "0.1.0"
2
+ VERSION = "0.1.1"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: paper_view
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Leon Vogt