feedback_engine 0.3.0 → 0.4.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/CHANGELOG.md +19 -2
- data/README.md +29 -7
- data/app/views/feedback_engine/feedbacks/index.html.erb +1 -1
- data/app/views/layouts/feedback_engine/application.html.erb +3 -2
- data/config/locales/feedback_engine.en.yml +1 -0
- data/lib/feedback_engine/version.rb +1 -1
- data/{app/assets → lib}/feedback_engine/widget.js +6 -86
- data/lib/feedback_engine/widget.rb +4 -2
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0dbeb9cc3333acd2485439cb1896536b2617595f980da907d79ddf0cdb9ccb6c
|
|
4
|
+
data.tar.gz: a7f6907958921185994ea549d3a6ed987c567a4cc17019e281a8cf821fcbb89f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8f9bc37eeafccb24cac72fc0ce2099a604e66325e9b4e966cebe622688517575c68154b6b9dce8fdc9b1791b3ed9bba1118fccf0c9e11b1bf6b4c229c003cadf
|
|
7
|
+
data.tar.gz: 8ad4196a942eab2ce07a17a3fe9048b27015ca769251df5c777876914e7cbb93aa5e3f0168a13ac38ba920e55fd129d9c23d1064f3368eaadbacee25287da256
|
data/CHANGELOG.md
CHANGED
|
@@ -1,11 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.1 (2026-07-23)
|
|
4
|
+
|
|
5
|
+
- The widget JavaScript moved from `app/assets/` to `lib/` (it was always
|
|
6
|
+
inlined server-side, never served as an asset). Rails auto-registers every
|
|
7
|
+
engine's `app/assets/*` directory with the host's asset pipeline, so
|
|
8
|
+
Propshaft hosts were ingesting the file into their asset namespace under
|
|
9
|
+
the bare logical name `widget.js` — colliding with any other gem or host
|
|
10
|
+
file of the same name — and needlessly digesting a public copy at
|
|
11
|
+
precompile. The gem is now invisible to Sprockets/Propshaft entirely.
|
|
12
|
+
No behavior change.
|
|
13
|
+
|
|
14
|
+
## 0.4.0 (2026-07-22)
|
|
15
|
+
|
|
16
|
+
- Removed the clipboard-paste / drag-and-drop / file-chips screenshot intake
|
|
17
|
+
added in 0.3.0 — pasted files all arrive as `image.png` and the extra UI
|
|
18
|
+
wasn't worth its complexity. Screenshots are back to a plain file input.
|
|
19
|
+
|
|
3
20
|
## 0.3.0 (2026-07-22)
|
|
4
21
|
|
|
5
|
-
- Screenshots can
|
|
22
|
+
- Screenshots can be pasted (Cmd/Ctrl+V) or dragged & dropped anywhere on
|
|
6
23
|
the feedback form, not just picked via the file dialog. Selected files show
|
|
7
24
|
as removable chips; non-images are ignored and the configured maximum is
|
|
8
|
-
enforced at intake.
|
|
25
|
+
enforced at intake. (Removed again in 0.4.0.)
|
|
9
26
|
- The dialog traps Tab focus while open and is labelled for screen readers
|
|
10
27
|
(`aria-labelledby`).
|
|
11
28
|
- Dashboard search across message, author, and section (case-insensitive,
|
data/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# feedback_engine
|
|
2
2
|
|
|
3
3
|
[](https://rubygems.org/gems/feedback_engine)
|
|
4
|
+
[](https://rubygems.org/gems/feedback_engine)
|
|
4
5
|
[](https://github.com/yshmarov/feedback-engine/actions/workflows/ci.yml)
|
|
5
6
|
[](MIT-LICENSE)
|
|
6
7
|
|
|
@@ -12,6 +13,30 @@ every submission in your own database. A minimal built-in dashboard lets you
|
|
|
12
13
|
browse and triage what users send. No third-party service, no data leaves your
|
|
13
14
|
app.
|
|
14
15
|
|
|
16
|
+
<picture>
|
|
17
|
+
<source media="(prefers-color-scheme: dark)" srcset="demo-widget-dark.png">
|
|
18
|
+
<img alt="The feedback widget: type, section, message, and screenshots" src="demo-widget-light.png" width="420">
|
|
19
|
+
</picture>
|
|
20
|
+
|
|
21
|
+
<picture>
|
|
22
|
+
<source media="(prefers-color-scheme: dark)" srcset="demo-dashboard-dark.png">
|
|
23
|
+
<img alt="The built-in triage dashboard: status tabs, filters, search" src="demo-dashboard-light.png" width="820">
|
|
24
|
+
</picture>
|
|
25
|
+
|
|
26
|
+
Both the widget and the dashboard follow the viewer's system appearance —
|
|
27
|
+
these screenshots are the same pages in light and dark mode.
|
|
28
|
+
|
|
29
|
+
## Why not Canny (or another feedback SaaS)?
|
|
30
|
+
|
|
31
|
+
Hosted feedback tools are great until you notice the trade: your users'
|
|
32
|
+
feedback lives in someone else's database, behind someone else's login, at a
|
|
33
|
+
per-seat price. `feedback_engine` keeps the core loop — collect, triage,
|
|
34
|
+
resolve — inside your app: **your database, your users, your auth, one gem.**
|
|
35
|
+
Attribution plugs into whatever authentication you already have, gating is a
|
|
36
|
+
lambda, and submissions are ordinary ActiveRecord rows you can query, export,
|
|
37
|
+
or wire into Slack. (A public voting board à la Canny is on the
|
|
38
|
+
[roadmap](https://github.com/yshmarov/feedback-engine/issues/1).)
|
|
39
|
+
|
|
15
40
|
- **Zero UI dependencies.** The widget is plain JavaScript and styles itself.
|
|
16
41
|
No Tailwind, no Stimulus, no importmap, no build step. The dashboard renders
|
|
17
42
|
its own styles too.
|
|
@@ -20,8 +45,7 @@ app.
|
|
|
20
45
|
- **Trigger it your way.** Use the built-in floating button, or hide it and
|
|
21
46
|
open the form from any element with a `data-feedback-engine-open` attribute.
|
|
22
47
|
- **Screenshots included.** Users can attach up to 3 images (via Active
|
|
23
|
-
Storage) —
|
|
24
|
-
configurable and enforced server-side.
|
|
48
|
+
Storage) — limits are configurable and enforced server-side.
|
|
25
49
|
- **Pluggable gating and attribution.** You decide who can send feedback, who
|
|
26
50
|
can read it, and how a submission is attributed to a user.
|
|
27
51
|
- **Localized.** The widget follows your app's `I18n.locale`; translations ship
|
|
@@ -177,11 +201,9 @@ end
|
|
|
177
201
|
### Screenshots
|
|
178
202
|
|
|
179
203
|
Uploads use Active Storage: `bin/rails active_storage:install` if you haven't
|
|
180
|
-
already.
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
as a hint in the widget. If Active Storage isn't loaded, the upload control
|
|
184
|
-
simply doesn't render and uploads are rejected.
|
|
204
|
+
already. Limits (`max_screenshots`, `max_screenshot_size`) are enforced
|
|
205
|
+
server-side and shown as a hint in the widget. If Active Storage isn't loaded,
|
|
206
|
+
the upload control simply doesn't render and uploads are rejected.
|
|
185
207
|
|
|
186
208
|
### Notifications
|
|
187
209
|
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
<thead>
|
|
33
33
|
<tr>
|
|
34
34
|
<th><%= t('feedback_engine.kind', default: 'Type') %></th>
|
|
35
|
-
<th><%= t('feedback_engine.message', default: 'Message') %></th>
|
|
35
|
+
<th><%= t('feedback_engine.dashboard.message', default: 'Message') %></th>
|
|
36
36
|
<th><%= t('feedback_engine.section', default: 'Section') %></th>
|
|
37
37
|
<th><%= t('feedback_engine.dashboard.from', default: 'From') %></th>
|
|
38
38
|
<th><%= t('feedback_engine.dashboard.filed', default: 'Filed') %></th>
|
|
@@ -33,8 +33,9 @@
|
|
|
33
33
|
.count { display: inline-block; min-width: 20px; padding: 0 6px; margin-left: 4px; border-radius: 999px;
|
|
34
34
|
background: var(--border); font-size: 12px; text-align: center; }
|
|
35
35
|
.filters { margin-bottom: 16px; display: flex; gap: 8px; align-items: center; }
|
|
36
|
-
.filters select { padding: 6px 8px; border: 1px solid var(--border);
|
|
37
|
-
background: var(--surface); color: var(--text); }
|
|
36
|
+
.filters select, .filters input[type=search] { padding: 6px 8px; border: 1px solid var(--border);
|
|
37
|
+
border-radius: 8px; background: var(--surface); color: var(--text); font: inherit; }
|
|
38
|
+
.filters input[type=search] { flex: 1; max-width: 320px; }
|
|
38
39
|
.card { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; overflow: hidden; }
|
|
39
40
|
table { width: 100%; border-collapse: collapse; }
|
|
40
41
|
th, td { padding: 10px 14px; text-align: left; vertical-align: top; }
|
|
@@ -20,8 +20,6 @@
|
|
|
20
20
|
var Z = 2147483000;
|
|
21
21
|
var overlay = null;
|
|
22
22
|
var lastFocused = null;
|
|
23
|
-
var fileInput = null;
|
|
24
|
-
var fileChips = null;
|
|
25
23
|
|
|
26
24
|
function ready(fn) {
|
|
27
25
|
if (document.readyState === "loading") {
|
|
@@ -114,25 +112,6 @@
|
|
|
114
112
|
// Keep Tab (and Shift+Tab) cycling inside the dialog while it is open.
|
|
115
113
|
overlay.addEventListener("keydown", trapFocus);
|
|
116
114
|
|
|
117
|
-
// Screenshots can be pasted (Cmd/Ctrl+V) or dropped anywhere on the form —
|
|
118
|
-
// the file picker is just one way in.
|
|
119
|
-
if (config.screenshots.enabled) {
|
|
120
|
-
dialog.addEventListener("paste", function (event) {
|
|
121
|
-
var files = event.clipboardData && event.clipboardData.files;
|
|
122
|
-
if (files && files.length) {
|
|
123
|
-
event.preventDefault();
|
|
124
|
-
addFiles(files);
|
|
125
|
-
}
|
|
126
|
-
});
|
|
127
|
-
dialog.addEventListener("dragover", function (event) {
|
|
128
|
-
event.preventDefault();
|
|
129
|
-
});
|
|
130
|
-
dialog.addEventListener("drop", function (event) {
|
|
131
|
-
event.preventDefault();
|
|
132
|
-
if (event.dataTransfer) addFiles(event.dataTransfer.files);
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
|
|
136
115
|
var first = dialog.querySelector("select, textarea, input");
|
|
137
116
|
if (first) first.focus();
|
|
138
117
|
}
|
|
@@ -141,8 +120,6 @@
|
|
|
141
120
|
if (!overlay) return;
|
|
142
121
|
overlay.remove();
|
|
143
122
|
overlay = null;
|
|
144
|
-
fileInput = null;
|
|
145
|
-
fileChips = null;
|
|
146
123
|
if (lastFocused && lastFocused.focus) lastFocused.focus();
|
|
147
124
|
}
|
|
148
125
|
|
|
@@ -265,69 +242,19 @@
|
|
|
265
242
|
}
|
|
266
243
|
|
|
267
244
|
function screenshotsField() {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
var wrap = field(config.labels.screenshots, fileInput);
|
|
245
|
+
var input = document.createElement("input");
|
|
246
|
+
input.type = "file";
|
|
247
|
+
input.name = "screenshots";
|
|
248
|
+
input.multiple = true;
|
|
249
|
+
input.accept = "image/*";
|
|
250
|
+
var wrap = field(config.labels.screenshots, input);
|
|
275
251
|
var hint = document.createElement("span");
|
|
276
252
|
hint.className = "fbe-hint";
|
|
277
253
|
hint.textContent = config.labels.screenshotsHint;
|
|
278
254
|
wrap.appendChild(hint);
|
|
279
|
-
fileChips = document.createElement("ul");
|
|
280
|
-
fileChips.className = "fbe-chips";
|
|
281
|
-
wrap.appendChild(fileChips);
|
|
282
255
|
return wrap;
|
|
283
256
|
}
|
|
284
257
|
|
|
285
|
-
// Merge pasted/dropped images into the file input (the single source of
|
|
286
|
-
// truth for what gets uploaded), capped at the configured maximum.
|
|
287
|
-
function addFiles(files) {
|
|
288
|
-
if (!fileInput) return;
|
|
289
|
-
var transfer = new DataTransfer();
|
|
290
|
-
var current = Array.prototype.slice.call(fileInput.files);
|
|
291
|
-
var incoming = Array.prototype.slice.call(files).filter(function (file) {
|
|
292
|
-
return /^image\//.test(file.type);
|
|
293
|
-
});
|
|
294
|
-
current.concat(incoming).slice(0, config.screenshots.max).forEach(function (file) {
|
|
295
|
-
transfer.items.add(file);
|
|
296
|
-
});
|
|
297
|
-
fileInput.files = transfer.files;
|
|
298
|
-
renderFileChips();
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
function removeFile(index) {
|
|
302
|
-
if (!fileInput) return;
|
|
303
|
-
var transfer = new DataTransfer();
|
|
304
|
-
Array.prototype.slice.call(fileInput.files).forEach(function (file, i) {
|
|
305
|
-
if (i !== index) transfer.items.add(file);
|
|
306
|
-
});
|
|
307
|
-
fileInput.files = transfer.files;
|
|
308
|
-
renderFileChips();
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
function renderFileChips() {
|
|
312
|
-
if (!fileChips) return;
|
|
313
|
-
fileChips.textContent = "";
|
|
314
|
-
Array.prototype.slice.call(fileInput.files).forEach(function (file, index) {
|
|
315
|
-
var chip = document.createElement("li");
|
|
316
|
-
var name = document.createElement("span");
|
|
317
|
-
name.textContent = file.name;
|
|
318
|
-
var remove = document.createElement("button");
|
|
319
|
-
remove.type = "button";
|
|
320
|
-
remove.setAttribute("aria-label", config.labels.close + " " + file.name);
|
|
321
|
-
remove.textContent = "×";
|
|
322
|
-
remove.addEventListener("click", function () {
|
|
323
|
-
removeFile(index);
|
|
324
|
-
});
|
|
325
|
-
chip.appendChild(name);
|
|
326
|
-
chip.appendChild(remove);
|
|
327
|
-
fileChips.appendChild(chip);
|
|
328
|
-
});
|
|
329
|
-
}
|
|
330
|
-
|
|
331
258
|
// --- submit -------------------------------------------------------------------
|
|
332
259
|
|
|
333
260
|
function submit(form) {
|
|
@@ -436,12 +363,6 @@
|
|
|
436
363
|
"border:1px solid #d1d5db;border-radius:6px;background:none;color:#1c2024;font:inherit;cursor:pointer}",
|
|
437
364
|
"#fbe-dialog textarea{resize:vertical}",
|
|
438
365
|
"#fbe-dialog .fbe-hint{display:block;margin-top:4px;font-size:12px;color:#6b7280;font-weight:400}",
|
|
439
|
-
"#fbe-dialog .fbe-chips{list-style:none;margin:6px 0 0;padding:0;display:flex;flex-wrap:wrap;gap:6px}",
|
|
440
|
-
"#fbe-dialog .fbe-chips li{display:flex;align-items:center;gap:4px;max-width:100%;",
|
|
441
|
-
"padding:2px 4px 2px 10px;border:1px solid #d1d5db;border-radius:999px;font-size:12px;font-weight:400}",
|
|
442
|
-
"#fbe-dialog .fbe-chips span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:180px}",
|
|
443
|
-
"#fbe-dialog .fbe-chips button{border:0;background:none;color:inherit;cursor:pointer;",
|
|
444
|
-
"font-size:14px;line-height:1;padding:2px 6px}",
|
|
445
366
|
"#fbe-dialog .fbe-error{color:#dc2626;margin:0 0 12px}",
|
|
446
367
|
"#fbe-dialog .fbe-actions{display:flex;justify-content:flex-end;gap:8px}",
|
|
447
368
|
"#fbe-dialog button{padding:8px 14px;border-radius:8px;cursor:pointer;font:inherit}",
|
|
@@ -454,7 +375,6 @@
|
|
|
454
375
|
"#fbe-dialog select,#fbe-dialog textarea,#fbe-dialog input[type=file]{border-color:#2a313a}",
|
|
455
376
|
"#fbe-dialog input[type=file]{color:#9aa2ab}",
|
|
456
377
|
"#fbe-dialog input[type=file]::file-selector-button{border-color:#2a313a;color:#e6e8ea}",
|
|
457
|
-
"#fbe-dialog .fbe-chips li{border-color:#2a313a}",
|
|
458
378
|
"#fbe-dialog .fbe-secondary{border-color:#2a313a}",
|
|
459
379
|
"#fbe-dialog .fbe-hint{color:#9aa2ab}",
|
|
460
380
|
"}"
|
|
@@ -7,9 +7,11 @@ module FeedbackEngine
|
|
|
7
7
|
# framework, no build step) and styles itself inline, so it drops into any
|
|
8
8
|
# Rails app regardless of its CSS or JS setup. It is inlined into the page
|
|
9
9
|
# rather than served as a separate asset to avoid any dependency on the
|
|
10
|
-
# host's asset pipeline
|
|
10
|
+
# host's asset pipeline — and it lives under lib/ (not app/assets/) so a
|
|
11
|
+
# host that *does* run a pipeline never ingests it either: nothing lands in
|
|
12
|
+
# the host's asset namespace or precompiled output.
|
|
11
13
|
module Widget
|
|
12
|
-
SOURCE = File.expand_path('
|
|
14
|
+
SOURCE = File.expand_path('widget.js', __dir__)
|
|
13
15
|
|
|
14
16
|
# Right-to-left scripts, so the form renders mirrored for those locales.
|
|
15
17
|
# Matched on the language subtag, so region variants ("ar-EG") count too.
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: feedback_engine
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yaroslav Shmarov
|
|
@@ -38,7 +38,6 @@ files:
|
|
|
38
38
|
- MIT-LICENSE
|
|
39
39
|
- README.md
|
|
40
40
|
- Rakefile
|
|
41
|
-
- app/assets/feedback_engine/widget.js
|
|
42
41
|
- app/controllers/feedback_engine/application_controller.rb
|
|
43
42
|
- app/controllers/feedback_engine/feedbacks_controller.rb
|
|
44
43
|
- app/controllers/feedback_engine/screenshots_controller.rb
|
|
@@ -79,6 +78,7 @@ files:
|
|
|
79
78
|
- lib/feedback_engine/configuration.rb
|
|
80
79
|
- lib/feedback_engine/engine.rb
|
|
81
80
|
- lib/feedback_engine/version.rb
|
|
81
|
+
- lib/feedback_engine/widget.js
|
|
82
82
|
- lib/feedback_engine/widget.rb
|
|
83
83
|
- lib/generators/feedback_engine/install/install_generator.rb
|
|
84
84
|
- lib/generators/feedback_engine/install/templates/create_feedback_engine_feedbacks.rb.tt
|
|
@@ -87,8 +87,10 @@ homepage: https://github.com/yshmarov/feedback-engine
|
|
|
87
87
|
licenses:
|
|
88
88
|
- MIT
|
|
89
89
|
metadata:
|
|
90
|
+
homepage_uri: https://github.com/yshmarov/feedback-engine
|
|
90
91
|
source_code_uri: https://github.com/yshmarov/feedback-engine
|
|
91
92
|
changelog_uri: https://github.com/yshmarov/feedback-engine/blob/main/CHANGELOG.md
|
|
93
|
+
bug_tracker_uri: https://github.com/yshmarov/feedback-engine/issues
|
|
92
94
|
rubygems_mfa_required: 'true'
|
|
93
95
|
rdoc_options: []
|
|
94
96
|
require_paths:
|