ideasbugs 0.7.2 → 0.7.4
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 +22 -0
- data/README.md +1 -0
- data/app/controllers/ideasbugs/feedbacks_controller.rb +2 -0
- data/app/controllers/ideasbugs/widgets_controller.rb +14 -4
- data/app/models/ideasbugs/feedback.rb +5 -1
- data/app/views/ideasbugs/feedbacks/_feedback_panel.html.erb +65 -0
- data/app/views/ideasbugs/feedbacks/index.html.erb +89 -72
- data/app/views/ideasbugs/feedbacks/show.html.erb +3 -58
- data/app/views/layouts/ideasbugs/application.html.erb +4 -64
- data/config/routes.rb +1 -0
- data/lib/generators/ideasbugs/install/install_generator.rb +1 -1
- data/lib/generators/ideasbugs/install/templates/initializer.rb +7 -1
- data/lib/ideasbugs/configuration.rb +5 -0
- data/lib/ideasbugs/dashboard.css +143 -0
- data/lib/ideasbugs/engine.rb +9 -0
- data/lib/ideasbugs/version.rb +1 -1
- data/lib/ideasbugs/widget.rb +9 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dd02a4b6d94f04c5fe4533f1f9fe3e51a8d7d5a0d9508657fe0b01fc2ac83abe
|
|
4
|
+
data.tar.gz: 6308208667e3d63aff2ec4750811e2a781ce06722ce54b8f4b44df0dba373171
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a9b8082e850661015f81cbc9b0700f78a1e0eb2d4e0d155b75e761640270f01b5d670f219ac464a8c28346be7a6c4cc7d0d4acf88001fb4374cfe235abd6c228
|
|
7
|
+
data.tar.gz: 5f7f874294bf4bd272aa1e31712e36595f7aa5911ce2ca6a1c5cf11be619d3f12616c7288b082aaea6f73d9c91f55ad269f5f3b015e1d0258ecc7d2cf3f47fef
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.4 (2026-07-31)
|
|
4
|
+
|
|
5
|
+
- Redesigned the admin feedback dashboard into a two-column triage layout with
|
|
6
|
+
status filters, a selected-record pane, and refreshed before/after
|
|
7
|
+
screenshots.
|
|
8
|
+
- Kept standalone feedback show pages working independently, including
|
|
9
|
+
scrollable detail content on narrow screens.
|
|
10
|
+
|
|
11
|
+
## 0.7.3 (2026-07-31)
|
|
12
|
+
|
|
13
|
+
- Added `mount_ideasbugs at: "/feedback"` as the install-time route helper,
|
|
14
|
+
keeping `config.mount_path` synchronized with the mounted engine path while
|
|
15
|
+
preserving manual `mount Ideasbugs::Engine` compatibility.
|
|
16
|
+
- Extracted the dashboard stylesheet into a same-origin, fingerprinted
|
|
17
|
+
`/dashboard.css` endpoint and added CSP meta tags to the dashboard layout.
|
|
18
|
+
The public widget remains pipeline-free and controller-served.
|
|
19
|
+
- New `config.storage_service`: store screenshots on a named Active Storage
|
|
20
|
+
service from the host's `config/storage.yml` instead of the environment
|
|
21
|
+
default. Point it at a dedicated bucket, folder, or provider-specific service
|
|
22
|
+
entry to keep feedback screenshots separate from the rest of the app's media.
|
|
23
|
+
`nil` keeps today's behavior.
|
|
24
|
+
|
|
3
25
|
## 0.7.2 (2026-07-30)
|
|
4
26
|
|
|
5
27
|
- Shipped the `IdeasBugs` admin dashboard title key in every bundled locale,
|
data/README.md
CHANGED
|
@@ -96,6 +96,7 @@ Everything is optional — a fresh install works with zero config. In
|
|
|
96
96
|
| `screenshots` | `true` | Allow uploads (needs Active Storage) |
|
|
97
97
|
| `max_screenshots` | `3` | Enforced server-side |
|
|
98
98
|
| `max_screenshot_size` | `5.megabytes` | Enforced server-side |
|
|
99
|
+
| `storage_service` | app default | Active Storage service for screenshots (`storage.yml` key) |
|
|
99
100
|
| `rate_limit` | `{ to: 10, within: 1.minute }` | Per-IP throttle (Rails 7.2+). `nil` disables |
|
|
100
101
|
| `show_button` | `true` | The floating button. `false` to use your own trigger |
|
|
101
102
|
| `button_label` | `nil` | Fixed button text. `nil` uses the localized default |
|
|
@@ -36,6 +36,8 @@ module Ideasbugs
|
|
|
36
36
|
@more = @feedbacks.size > PER_PAGE
|
|
37
37
|
@feedbacks = @feedbacks.first(PER_PAGE)
|
|
38
38
|
|
|
39
|
+
@selected_feedback = tenant_scope.find_by(id: params[:feedback_id]) if params[:feedback_id].present?
|
|
40
|
+
|
|
39
41
|
# Only surface the Section column when it can carry information: the host
|
|
40
42
|
# configured sections, or some record already has one (e.g. sections were
|
|
41
43
|
# configured historically). Otherwise it's a permanently blank column.
|
|
@@ -7,7 +7,7 @@ module Ideasbugs
|
|
|
7
7
|
# Drive swaps the <body> and re-runs body scripts under the *original*
|
|
8
8
|
# page's CSP header, where a freshly minted inline nonce would be refused.
|
|
9
9
|
class WidgetsController < ApplicationController
|
|
10
|
-
#
|
|
10
|
+
# These assets are static and carry no user data; without this, Rails'
|
|
11
11
|
# cross-origin JavaScript guard refuses to serve it to a plain
|
|
12
12
|
# <script src> request.
|
|
13
13
|
skip_forgery_protection
|
|
@@ -17,10 +17,20 @@ module Ideasbugs
|
|
|
17
17
|
# fingerprinted URL is immutable and gets long-lived caching; anything
|
|
18
18
|
# else only ETag-revalidates.
|
|
19
19
|
def show
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
serve(Widget.javascript, Widget.fingerprint, 'text/javascript')
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def dashboard_stylesheet
|
|
24
|
+
serve(Widget.dashboard_stylesheet, Widget.dashboard_stylesheet_fingerprint, 'text/css')
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def serve(source, fingerprint, content_type)
|
|
30
|
+
expires_in 1.year, public: true if params[:v] == fingerprint
|
|
31
|
+
return unless stale?(etag: [Ideasbugs::VERSION, fingerprint])
|
|
22
32
|
|
|
23
|
-
render plain:
|
|
33
|
+
render plain: source, content_type: content_type
|
|
24
34
|
end
|
|
25
35
|
end
|
|
26
36
|
end
|
|
@@ -9,7 +9,11 @@ module Ideasbugs
|
|
|
9
9
|
# as a scope name, and three statuses don't need the machinery anyway.
|
|
10
10
|
STATUSES = %w[open in_review resolved].freeze
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
if defined?(::ActiveStorage)
|
|
13
|
+
# Read at class load, after the host's initializer has run. nil falls
|
|
14
|
+
# through to Active Storage's environment default service.
|
|
15
|
+
has_many_attached :screenshots, service: Ideasbugs.config.storage_service
|
|
16
|
+
end
|
|
13
17
|
|
|
14
18
|
validates :message, presence: true
|
|
15
19
|
validates :status, inclusion: { in: STATUSES }
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<section class="feedback-panel">
|
|
2
|
+
<div class="panel-head">
|
|
3
|
+
<h1>
|
|
4
|
+
<span class="badge kind-<%= feedback.kind.to_s.parameterize %>">
|
|
5
|
+
<%= t("ideasbugs.kinds.#{feedback.kind}", default: feedback.kind.humanize) %>
|
|
6
|
+
</span>
|
|
7
|
+
<span class="badge status-<%= feedback.status %>">
|
|
8
|
+
<%= t("ideasbugs.statuses.#{feedback.status}", default: feedback.status.humanize) %>
|
|
9
|
+
</span>
|
|
10
|
+
<span class="case-id">#<%= feedback.id %></span>
|
|
11
|
+
</h1>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<div class="detail-scroll">
|
|
15
|
+
<div class="card pad">
|
|
16
|
+
<dl>
|
|
17
|
+
<% if feedback.section.present? %>
|
|
18
|
+
<dt><%= t('ideasbugs.section', default: 'Section') %></dt>
|
|
19
|
+
<dd><%= feedback.section %></dd>
|
|
20
|
+
<% end %>
|
|
21
|
+
<% if feedback.author_label.present? || feedback.author_id.present? %>
|
|
22
|
+
<dt><%= t('ideasbugs.dashboard.from', default: 'From') %></dt>
|
|
23
|
+
<dd><%= feedback.author_label.presence || "##{feedback.author_id}" %></dd>
|
|
24
|
+
<% end %>
|
|
25
|
+
<% if feedback.page_url.present? %>
|
|
26
|
+
<dt><%= t('ideasbugs.dashboard.page', default: 'Page') %></dt>
|
|
27
|
+
<dd><%= link_to feedback.page_url, feedback.page_url, target: '_blank', rel: 'noopener' %></dd>
|
|
28
|
+
<% end %>
|
|
29
|
+
<% if feedback.user_agent.present? %>
|
|
30
|
+
<dt><%= t('ideasbugs.dashboard.browser', default: 'Browser') %></dt>
|
|
31
|
+
<dd class="muted"><%= feedback.user_agent %></dd>
|
|
32
|
+
<% end %>
|
|
33
|
+
<dt><%= t('ideasbugs.dashboard.filed', default: 'Filed') %></dt>
|
|
34
|
+
<dd><%= feedback.created_at %></dd>
|
|
35
|
+
</dl>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<div class="card pad">
|
|
39
|
+
<p class="message"><%= feedback.message %></p>
|
|
40
|
+
|
|
41
|
+
<% if feedback.screenshots? %>
|
|
42
|
+
<div class="shots">
|
|
43
|
+
<% feedback.screenshots.each do |screenshot| %>
|
|
44
|
+
<%= link_to feedback_screenshot_path(feedback, screenshot.id), target: '_blank', rel: 'noopener' do %>
|
|
45
|
+
<%= image_tag feedback_screenshot_path(feedback, screenshot.id), alt: screenshot.filename %>
|
|
46
|
+
<% end %>
|
|
47
|
+
<% end %>
|
|
48
|
+
</div>
|
|
49
|
+
<% end %>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<div class="actions">
|
|
53
|
+
<% (Ideasbugs::Feedback::STATUSES - [feedback.status]).each do |status| %>
|
|
54
|
+
<%= button_to t("ideasbugs.dashboard.move_to_#{status}", default: "Mark #{status.humanize.downcase}"),
|
|
55
|
+
feedback_path(feedback),
|
|
56
|
+
method: :patch, params: { feedback: { status: status } },
|
|
57
|
+
class: ('primary' if status == 'resolved') %>
|
|
58
|
+
<% end %>
|
|
59
|
+
<%= button_to t('ideasbugs.dashboard.delete', default: 'Delete'),
|
|
60
|
+
feedback_path(feedback),
|
|
61
|
+
method: :delete, class: 'danger',
|
|
62
|
+
form: { onsubmit: "return confirm('#{t('ideasbugs.dashboard.delete_confirm', default: 'Delete this feedback?')}')" } %>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</section>
|
|
@@ -1,82 +1,99 @@
|
|
|
1
|
+
<% content_for :body_class, 'ib-index' %>
|
|
2
|
+
|
|
1
3
|
<h1><%= t('ideasbugs.dashboard.title', default: 'IdeasBugs') %></h1>
|
|
2
4
|
|
|
3
|
-
<div class="
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
<div class="triage-shell <%= 'has-selected' if @selected_feedback %>">
|
|
6
|
+
<aside class="triage-sidebar" aria-label="<%= t('ideasbugs.dashboard.feedback', default: 'Feedback') %>">
|
|
7
|
+
<div class="tabs">
|
|
8
|
+
<% Ideasbugs::Feedback::STATUSES.each do |status| %>
|
|
9
|
+
<%= link_to feedbacks_path(status: status, kind: @kind, q: @query),
|
|
10
|
+
class: ('active' if status == @status) do %>
|
|
11
|
+
<%= t("ideasbugs.statuses.#{status}", default: status.humanize) %>
|
|
12
|
+
<span class="count"><%= @counts.fetch(status, 0) %></span>
|
|
13
|
+
<% end %>
|
|
14
|
+
<% end %>
|
|
15
|
+
</div>
|
|
12
16
|
|
|
13
|
-
<form class="filters" method="get">
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
</form>
|
|
17
|
+
<form class="filters" method="get">
|
|
18
|
+
<input type="hidden" name="status" value="<%= @status %>">
|
|
19
|
+
<select name="kind" onchange="this.form.submit()">
|
|
20
|
+
<option value=""><%= t('ideasbugs.dashboard.all_kinds', default: 'All types') %></option>
|
|
21
|
+
<% Ideasbugs.config.kinds.each do |kind| %>
|
|
22
|
+
<option value="<%= kind %>" <%= 'selected' if kind.to_s == @kind %>>
|
|
23
|
+
<%= t("ideasbugs.kinds.#{kind}", default: kind.to_s.humanize) %>
|
|
24
|
+
</option>
|
|
25
|
+
<% end %>
|
|
26
|
+
</select>
|
|
27
|
+
<input type="search" name="q" value="<%= @query %>"
|
|
28
|
+
placeholder="<%= t('ideasbugs.dashboard.search_placeholder', default: 'Search message, author, section…') %>"
|
|
29
|
+
aria-label="<%= t('ideasbugs.dashboard.search', default: 'Search') %>">
|
|
30
|
+
<button><%= t('ideasbugs.dashboard.search', default: 'Search') %></button>
|
|
31
|
+
</form>
|
|
28
32
|
|
|
29
|
-
<div class="
|
|
30
|
-
|
|
31
|
-
<table>
|
|
32
|
-
<thead>
|
|
33
|
-
<tr>
|
|
34
|
-
<th><%= t('ideasbugs.kind', default: 'Type') %></th>
|
|
35
|
-
<th><%= t('ideasbugs.dashboard.message', default: 'Message') %></th>
|
|
36
|
-
<% if @show_section %>
|
|
37
|
-
<th><%= t('ideasbugs.section', default: 'Section') %></th>
|
|
38
|
-
<% end %>
|
|
39
|
-
<th><%= t('ideasbugs.dashboard.from', default: 'From') %></th>
|
|
40
|
-
<th><%= t('ideasbugs.dashboard.filed', default: 'Filed') %></th>
|
|
41
|
-
</tr>
|
|
42
|
-
</thead>
|
|
43
|
-
<tbody>
|
|
33
|
+
<div class="feedback-list">
|
|
34
|
+
<% if @feedbacks.any? %>
|
|
44
35
|
<% @feedbacks.each do |feedback| %>
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
36
|
+
<%= link_to feedbacks_path(status: @status, kind: @kind, q: @query, page: @page,
|
|
37
|
+
feedback_id: feedback.id),
|
|
38
|
+
class: ['feedback-row', ('active' if @selected_feedback&.id == feedback.id)].compact do %>
|
|
39
|
+
<span class="feedback-main">
|
|
40
|
+
<span class="feedback-topline">
|
|
41
|
+
<span class="badge kind-<%= feedback.kind.to_s.parameterize %>">
|
|
42
|
+
<%= t("ideasbugs.kinds.#{feedback.kind}", default: feedback.kind.humanize) %>
|
|
43
|
+
</span>
|
|
44
|
+
<span class="badge status-<%= feedback.status %>">
|
|
45
|
+
<%= t("ideasbugs.statuses.#{feedback.status}", default: feedback.status.humanize) %>
|
|
46
|
+
</span>
|
|
47
|
+
<span class="muted feedback-time" title="<%= feedback.created_at %>">
|
|
48
|
+
<%= time_ago_in_words(feedback.created_at) %>
|
|
49
|
+
</span>
|
|
50
|
+
</span>
|
|
51
|
+
<span class="feedback-message"><%= truncate(feedback.message, length: 120) %></span>
|
|
52
|
+
<span class="muted feedback-meta">
|
|
53
|
+
<span>#<%= feedback.id %></span>
|
|
54
|
+
<% if @show_section && feedback.section.present? %>
|
|
55
|
+
<span class="feedback-section"><%= feedback.section %></span>
|
|
56
|
+
<% end %>
|
|
57
|
+
<% if feedback.author_label.present? %>
|
|
58
|
+
<span><%= feedback.author_label %></span>
|
|
59
|
+
<% end %>
|
|
49
60
|
</span>
|
|
50
|
-
</
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
<% if feedback.screenshots? %>
|
|
54
|
-
<div class="muted">📎 <%= feedback.screenshots.count %></div>
|
|
55
|
-
<% end %>
|
|
56
|
-
</td>
|
|
57
|
-
<% if @show_section %>
|
|
58
|
-
<td><%= feedback.section %></td>
|
|
61
|
+
</span>
|
|
62
|
+
<% if feedback.screenshots? %>
|
|
63
|
+
<span class="muted attachment-count">📎 <%= feedback.screenshots.count %></span>
|
|
59
64
|
<% end %>
|
|
60
|
-
|
|
61
|
-
<td class="muted" title="<%= feedback.created_at %>">
|
|
62
|
-
<%= time_ago_in_words(feedback.created_at) %>
|
|
63
|
-
</td>
|
|
64
|
-
</tr>
|
|
65
|
+
<% end %>
|
|
65
66
|
<% end %>
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
67
|
+
<% else %>
|
|
68
|
+
<div class="empty"><%= t('ideasbugs.dashboard.empty', default: 'Nothing here yet.') %></div>
|
|
69
|
+
<% end %>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<div class="pager">
|
|
73
|
+
<% if @page > 1 %>
|
|
74
|
+
<%= link_to t('ideasbugs.dashboard.newer', default: '← Newer'),
|
|
75
|
+
feedbacks_path(status: @status, kind: @kind, q: @query, page: @page - 1) %>
|
|
76
|
+
<% end %>
|
|
77
|
+
<% if @more %>
|
|
78
|
+
<%= link_to t('ideasbugs.dashboard.older', default: 'Older →'),
|
|
79
|
+
feedbacks_path(status: @status, kind: @kind, q: @query, page: @page + 1) %>
|
|
80
|
+
<% end %>
|
|
81
|
+
</div>
|
|
82
|
+
</aside>
|
|
72
83
|
|
|
73
|
-
<
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
84
|
+
<main class="triage-detail" aria-label="<%= t('ideasbugs.dashboard.current_feedback', default: 'Current feedback') %>">
|
|
85
|
+
<% if @selected_feedback %>
|
|
86
|
+
<p class="mobile-back">
|
|
87
|
+
<%= link_to t('ideasbugs.dashboard.back', default: '← All feedback'),
|
|
88
|
+
feedbacks_path(status: @status, kind: @kind, q: @query, page: @page) %>
|
|
89
|
+
</p>
|
|
90
|
+
<%= render 'feedback_panel', feedback: @selected_feedback %>
|
|
91
|
+
<% else %>
|
|
92
|
+
<div class="empty-state">
|
|
93
|
+
<h2><%= t('ideasbugs.dashboard.select_feedback', default: 'Select feedback') %></h2>
|
|
94
|
+
<p class="muted"><%= t('ideasbugs.dashboard.select_feedback_hint',
|
|
95
|
+
default: 'Open an item from the list to triage it without leaving the queue.') %></p>
|
|
96
|
+
</div>
|
|
97
|
+
<% end %>
|
|
98
|
+
</main>
|
|
82
99
|
</div>
|
|
@@ -1,63 +1,8 @@
|
|
|
1
|
+
<% content_for :body_class, 'ib-show' %>
|
|
2
|
+
|
|
1
3
|
<div class="breadcrumb">
|
|
2
4
|
<%= link_to t('ideasbugs.dashboard.title', default: 'IdeasBugs'), root_path %>
|
|
3
5
|
/ #<%= @feedback.id %>
|
|
4
6
|
</div>
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
<span class="badge kind-<%= @feedback.kind.to_s.parameterize %>">
|
|
8
|
-
<%= t("ideasbugs.kinds.#{@feedback.kind}", default: @feedback.kind.humanize) %>
|
|
9
|
-
</span>
|
|
10
|
-
<span class="badge status-<%= @feedback.status %>">
|
|
11
|
-
<%= t("ideasbugs.statuses.#{@feedback.status}", default: @feedback.status.humanize) %>
|
|
12
|
-
</span>
|
|
13
|
-
</h1>
|
|
14
|
-
|
|
15
|
-
<div class="card" style="padding: 16px; margin-bottom: 16px;">
|
|
16
|
-
<p class="message"><%= @feedback.message %></p>
|
|
17
|
-
|
|
18
|
-
<% if @feedback.screenshots? %>
|
|
19
|
-
<div class="shots">
|
|
20
|
-
<% @feedback.screenshots.each do |screenshot| %>
|
|
21
|
-
<%= link_to feedback_screenshot_path(@feedback, screenshot.id), target: '_blank', rel: 'noopener' do %>
|
|
22
|
-
<%= image_tag feedback_screenshot_path(@feedback, screenshot.id), alt: screenshot.filename %>
|
|
23
|
-
<% end %>
|
|
24
|
-
<% end %>
|
|
25
|
-
</div>
|
|
26
|
-
<% end %>
|
|
27
|
-
</div>
|
|
28
|
-
|
|
29
|
-
<div class="card" style="padding: 16px;">
|
|
30
|
-
<dl>
|
|
31
|
-
<% if @feedback.section.present? %>
|
|
32
|
-
<dt><%= t('ideasbugs.section', default: 'Section') %></dt>
|
|
33
|
-
<dd><%= @feedback.section %></dd>
|
|
34
|
-
<% end %>
|
|
35
|
-
<% if @feedback.author_label.present? || @feedback.author_id.present? %>
|
|
36
|
-
<dt><%= t('ideasbugs.dashboard.from', default: 'From') %></dt>
|
|
37
|
-
<dd><%= @feedback.author_label.presence || "##{@feedback.author_id}" %></dd>
|
|
38
|
-
<% end %>
|
|
39
|
-
<% if @feedback.page_url.present? %>
|
|
40
|
-
<dt><%= t('ideasbugs.dashboard.page', default: 'Page') %></dt>
|
|
41
|
-
<dd><%= link_to @feedback.page_url, @feedback.page_url, target: '_blank', rel: 'noopener' %></dd>
|
|
42
|
-
<% end %>
|
|
43
|
-
<% if @feedback.user_agent.present? %>
|
|
44
|
-
<dt><%= t('ideasbugs.dashboard.browser', default: 'Browser') %></dt>
|
|
45
|
-
<dd class="muted"><%= @feedback.user_agent %></dd>
|
|
46
|
-
<% end %>
|
|
47
|
-
<dt><%= t('ideasbugs.dashboard.filed', default: 'Filed') %></dt>
|
|
48
|
-
<dd><%= @feedback.created_at %></dd>
|
|
49
|
-
</dl>
|
|
50
|
-
</div>
|
|
51
|
-
|
|
52
|
-
<div class="actions">
|
|
53
|
-
<% (Ideasbugs::Feedback::STATUSES - [@feedback.status]).each do |status| %>
|
|
54
|
-
<%= button_to t("ideasbugs.dashboard.move_to_#{status}", default: "Mark #{status.humanize.downcase}"),
|
|
55
|
-
feedback_path(@feedback),
|
|
56
|
-
method: :patch, params: { feedback: { status: status } },
|
|
57
|
-
class: ('primary' if status == 'resolved') %>
|
|
58
|
-
<% end %>
|
|
59
|
-
<%= button_to t('ideasbugs.dashboard.delete', default: 'Delete'),
|
|
60
|
-
feedback_path(@feedback),
|
|
61
|
-
method: :delete, class: 'danger',
|
|
62
|
-
form: { onsubmit: "return confirm('#{t('ideasbugs.dashboard.delete_confirm', default: 'Delete this feedback?')}')" } %>
|
|
63
|
-
</div>
|
|
8
|
+
<%= render 'feedback_panel', feedback: @feedback %>
|
|
@@ -4,71 +4,11 @@
|
|
|
4
4
|
<title><%= t('ideasbugs.dashboard.title', default: 'IdeasBugs') %></title>
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<%= csrf_meta_tags %>
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
--bg: #f6f7f9; --surface: #fff; --text: #1c2024; --muted: #6b7280;
|
|
11
|
-
--border: #e5e7eb; --accent: #2563eb; --accent-text: #fff;
|
|
12
|
-
--bug: #dc2626; --feature: #16a34a; --other: #6b7280;
|
|
13
|
-
--open: #2563eb; --in_review: #d97706; --resolved: #16a34a;
|
|
14
|
-
}
|
|
15
|
-
@media (prefers-color-scheme: dark) {
|
|
16
|
-
:root {
|
|
17
|
-
--bg: #111418; --surface: #1a1f26; --text: #e6e8ea; --muted: #9aa2ab;
|
|
18
|
-
--border: #2a313a; --accent: #3b82f6;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
* { box-sizing: border-box; }
|
|
22
|
-
body { margin: 0; background: var(--bg); color: var(--text);
|
|
23
|
-
font: 15px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif; }
|
|
24
|
-
a { color: var(--accent); text-decoration: none; }
|
|
25
|
-
a:hover { text-decoration: underline; }
|
|
26
|
-
.container { max-width: 960px; margin: 0 auto; padding: 24px 16px 64px; }
|
|
27
|
-
h1 { font-size: 22px; margin: 0 0 16px; }
|
|
28
|
-
.tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--border); margin-bottom: 16px; flex-wrap: wrap; }
|
|
29
|
-
.tabs a { padding: 8px 14px; border-radius: 8px 8px 0 0; color: var(--muted); }
|
|
30
|
-
.tabs a.active { color: var(--text); font-weight: 600; border: 1px solid var(--border);
|
|
31
|
-
border-bottom: 2px solid var(--surface); background: var(--surface); margin-bottom: -1px; }
|
|
32
|
-
.tabs a:hover { text-decoration: none; color: var(--text); }
|
|
33
|
-
.count { display: inline-block; min-width: 20px; padding: 0 6px; margin-left: 4px; border-radius: 999px;
|
|
34
|
-
background: var(--border); font-size: 12px; text-align: center; }
|
|
35
|
-
.filters { margin-bottom: 16px; display: flex; gap: 8px; align-items: center; }
|
|
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; }
|
|
39
|
-
.card { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; overflow: hidden; }
|
|
40
|
-
table { width: 100%; border-collapse: collapse; }
|
|
41
|
-
th, td { padding: 10px 14px; text-align: left; vertical-align: top; }
|
|
42
|
-
th { font-size: 12px; text-transform: uppercase; letter-spacing: .04em; color: var(--muted);
|
|
43
|
-
border-bottom: 1px solid var(--border); }
|
|
44
|
-
tr + tr td { border-top: 1px solid var(--border); }
|
|
45
|
-
.badge { display: inline-block; padding: 2px 10px; border-radius: 999px; font-size: 12px; font-weight: 600;
|
|
46
|
-
color: #fff; white-space: nowrap; }
|
|
47
|
-
.badge.kind-bug { background: var(--bug); }
|
|
48
|
-
.badge.kind-feature { background: var(--feature); }
|
|
49
|
-
.badge.kind-other, .badge.kind-default { background: var(--other); }
|
|
50
|
-
.badge.status-open { background: var(--open); }
|
|
51
|
-
.badge.status-in_review { background: var(--in_review); }
|
|
52
|
-
.badge.status-resolved { background: var(--resolved); }
|
|
53
|
-
.muted { color: var(--muted); font-size: 13px; }
|
|
54
|
-
.pager { margin-top: 16px; display: flex; gap: 16px; }
|
|
55
|
-
.actions { display: flex; gap: 8px; flex-wrap: wrap; margin: 16px 0; }
|
|
56
|
-
.actions form { display: inline; }
|
|
57
|
-
button, .button { padding: 8px 14px; border: 1px solid var(--border); border-radius: 8px; cursor: pointer;
|
|
58
|
-
background: var(--surface); color: var(--text); font: inherit; }
|
|
59
|
-
button.primary { background: var(--accent); border-color: var(--accent); color: var(--accent-text); }
|
|
60
|
-
button.danger { color: var(--bug); }
|
|
61
|
-
.message { white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
62
|
-
dl { display: grid; grid-template-columns: max-content 1fr; gap: 6px 16px; margin: 0; }
|
|
63
|
-
dt { color: var(--muted); }
|
|
64
|
-
dd { margin: 0; overflow-wrap: anywhere; }
|
|
65
|
-
.shots { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 8px; }
|
|
66
|
-
.shots img { max-width: 280px; max-height: 200px; border: 1px solid var(--border); border-radius: 8px; }
|
|
67
|
-
.empty { padding: 48px 16px; text-align: center; color: var(--muted); }
|
|
68
|
-
.breadcrumb { margin-bottom: 12px; font-size: 13px; }
|
|
69
|
-
</style>
|
|
7
|
+
<%= csp_meta_tag %>
|
|
8
|
+
<%= stylesheet_link_tag "#{dashboard_stylesheet_path}?v=#{Ideasbugs::Widget.dashboard_stylesheet_fingerprint}",
|
|
9
|
+
'data-turbo-track': 'reload' %>
|
|
70
10
|
</head>
|
|
71
|
-
<body>
|
|
11
|
+
<body class="<%= content_for?(:body_class) ? yield(:body_class) : '' %>">
|
|
72
12
|
<div class="container">
|
|
73
13
|
<%= yield %>
|
|
74
14
|
</div>
|
data/config/routes.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
Ideasbugs::Engine.routes.draw do
|
|
4
4
|
# The widget code, served same-origin (see WidgetsController for why).
|
|
5
5
|
get 'widget.js', to: 'widgets#show', as: :widget
|
|
6
|
+
get 'dashboard.css', to: 'widgets#dashboard_stylesheet', as: :dashboard_stylesheet
|
|
6
7
|
|
|
7
8
|
# create is the public widget endpoint; the rest is the triage dashboard.
|
|
8
9
|
resources :feedbacks, only: %i[create index show update destroy] do
|
|
@@ -47,6 +47,11 @@ Ideasbugs.configure do |config|
|
|
|
47
47
|
# config.screenshots = true
|
|
48
48
|
# config.max_screenshots = 3
|
|
49
49
|
# config.max_screenshot_size = 5.megabytes
|
|
50
|
+
#
|
|
51
|
+
# Store screenshots on a specific Active Storage service from
|
|
52
|
+
# config/storage.yml, such as a dedicated bucket or folder. Default: your
|
|
53
|
+
# environment's default service.
|
|
54
|
+
# config.storage_service = :ideasbugs_uploads
|
|
50
55
|
|
|
51
56
|
# Per-IP throttle for the submission endpoint (Rails 7.2+; ignored on 7.1).
|
|
52
57
|
# Keyword arguments for Rails' rate limiter; nil disables throttling.
|
|
@@ -59,7 +64,8 @@ Ideasbugs.configure do |config|
|
|
|
59
64
|
# Fixed button text; leave nil to use the localized default.
|
|
60
65
|
# config.button_label = nil
|
|
61
66
|
|
|
62
|
-
#
|
|
67
|
+
# Default mount path used by `mount_ideasbugs`.
|
|
68
|
+
# Override only if you mount the engine manually.
|
|
63
69
|
# config.mount_path = "/feedback"
|
|
64
70
|
|
|
65
71
|
# Called with each saved feedback — notify Slack, send an email, etc.
|
|
@@ -49,6 +49,10 @@ module Ideasbugs
|
|
|
49
49
|
# Upload limits, enforced server-side and mirrored in the widget.
|
|
50
50
|
attr_accessor :max_screenshots, :max_screenshot_size
|
|
51
51
|
|
|
52
|
+
# The Active Storage service that stores screenshots, as a service name
|
|
53
|
+
# from the host's config/storage.yml. nil uses the environment default.
|
|
54
|
+
attr_accessor :storage_service
|
|
55
|
+
|
|
52
56
|
# Per-IP throttle for the public submission endpoint, as keyword arguments
|
|
53
57
|
# for Rails' rate limiter (Rails 7.2+; ignored on 7.1). Read once when the
|
|
54
58
|
# controller loads — set it in an initializer. nil disables throttling.
|
|
@@ -83,6 +87,7 @@ module Ideasbugs
|
|
|
83
87
|
@screenshots = true
|
|
84
88
|
@max_screenshots = 3
|
|
85
89
|
@max_screenshot_size = 5 * 1024 * 1024
|
|
90
|
+
@storage_service = nil
|
|
86
91
|
@rate_limit = { to: 10, within: 60 }
|
|
87
92
|
@show_button = true
|
|
88
93
|
@button_label = nil
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
color-scheme: light dark;
|
|
3
|
+
--bg: #f6f7f9; --surface: #fff; --text: #1c2024; --muted: #6b7280;
|
|
4
|
+
--border: #e5e7eb; --accent: #2563eb; --accent-text: #fff;
|
|
5
|
+
--bug: #dc2626; --feature: #16a34a; --other: #6b7280;
|
|
6
|
+
--open: #2563eb; --in_review: #d97706; --resolved: #16a34a;
|
|
7
|
+
}
|
|
8
|
+
@media (prefers-color-scheme: dark) {
|
|
9
|
+
:root {
|
|
10
|
+
--bg: #111418; --surface: #1a1f26; --text: #e6e8ea; --muted: #9aa2ab;
|
|
11
|
+
--border: #2a313a; --accent: #3b82f6;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
* { box-sizing: border-box; }
|
|
15
|
+
body { margin: 0; background: var(--bg); color: var(--text);
|
|
16
|
+
font: 15px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif; }
|
|
17
|
+
a { color: var(--accent); text-decoration: none; }
|
|
18
|
+
a:hover { text-decoration: underline; }
|
|
19
|
+
.container { max-width: 960px; margin: 0 auto; padding: 24px 16px 64px; }
|
|
20
|
+
h1 { font-size: 22px; margin: 0 0 16px; }
|
|
21
|
+
.tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--border); margin-bottom: 16px; flex-wrap: wrap; }
|
|
22
|
+
.tabs a { padding: 8px 14px; border-radius: 8px 8px 0 0; color: var(--muted); }
|
|
23
|
+
.tabs a.active { color: var(--text); font-weight: 600; border: 1px solid var(--border);
|
|
24
|
+
border-bottom: 2px solid var(--surface); background: var(--surface); margin-bottom: -1px; }
|
|
25
|
+
.tabs a:hover { text-decoration: none; color: var(--text); }
|
|
26
|
+
.count { display: inline-block; min-width: 20px; padding: 0 6px; margin-left: 4px; border-radius: 999px;
|
|
27
|
+
background: var(--border); font-size: 12px; text-align: center; }
|
|
28
|
+
.filters { margin-bottom: 16px; display: flex; gap: 8px; align-items: center; }
|
|
29
|
+
.filters select, .filters input[type=search] { padding: 6px 8px; border: 1px solid var(--border);
|
|
30
|
+
border-radius: 8px; background: var(--surface); color: var(--text); font: inherit; }
|
|
31
|
+
.filters input[type=search] { flex: 1; max-width: 320px; }
|
|
32
|
+
.card { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; overflow: hidden; }
|
|
33
|
+
.card.pad { padding: 16px; }
|
|
34
|
+
table { width: 100%; border-collapse: collapse; }
|
|
35
|
+
th, td { padding: 10px 14px; text-align: left; vertical-align: top; }
|
|
36
|
+
th { font-size: 12px; text-transform: uppercase; letter-spacing: .04em; color: var(--muted);
|
|
37
|
+
border-bottom: 1px solid var(--border); }
|
|
38
|
+
tr + tr td { border-top: 1px solid var(--border); }
|
|
39
|
+
.badge { display: inline-block; padding: 2px 10px; border-radius: 999px; font-size: 12px; font-weight: 600;
|
|
40
|
+
color: #fff; white-space: nowrap; }
|
|
41
|
+
.badge.kind-bug { background: var(--bug); }
|
|
42
|
+
.badge.kind-feature { background: var(--feature); }
|
|
43
|
+
.badge.kind-other, .badge.kind-default { background: var(--other); }
|
|
44
|
+
.badge.status-open { background: var(--open); }
|
|
45
|
+
.badge.status-in_review { background: var(--in_review); }
|
|
46
|
+
.badge.status-resolved { background: var(--resolved); }
|
|
47
|
+
.muted { color: var(--muted); font-size: 13px; }
|
|
48
|
+
.pager { margin-top: 16px; display: flex; gap: 16px; }
|
|
49
|
+
.actions { display: flex; gap: 8px; flex-wrap: wrap; margin: 16px 0; }
|
|
50
|
+
.actions form { display: inline; }
|
|
51
|
+
button, .button { padding: 8px 14px; border: 1px solid var(--border); border-radius: 8px; cursor: pointer;
|
|
52
|
+
background: var(--surface); color: var(--text); font: inherit; }
|
|
53
|
+
button.primary { background: var(--accent); border-color: var(--accent); color: var(--accent-text); }
|
|
54
|
+
button.danger { color: var(--bug); }
|
|
55
|
+
.message { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
56
|
+
dl { display: grid; grid-template-columns: max-content 1fr; gap: 6px 16px; margin: 0; }
|
|
57
|
+
dt { color: var(--muted); }
|
|
58
|
+
dd { margin: 0; overflow-wrap: anywhere; }
|
|
59
|
+
.shots { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 8px; }
|
|
60
|
+
.shots img { max-width: 280px; max-height: 200px; border: 1px solid var(--border); border-radius: 8px; }
|
|
61
|
+
.empty { padding: 48px 16px; text-align: center; color: var(--muted); }
|
|
62
|
+
.breadcrumb { margin-bottom: 12px; font-size: 13px; }
|
|
63
|
+
.case-id { color: var(--muted); font-weight: 400; font-size: 15px; }
|
|
64
|
+
.panel-head { flex: 0 0 auto; }
|
|
65
|
+
.panel-head h1 { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }
|
|
66
|
+
.detail-scroll { display: flex; flex-direction: column; gap: 12px; min-height: 0; }
|
|
67
|
+
.feedback-panel { min-width: 0; display: flex; flex-direction: column; }
|
|
68
|
+
.feedback-panel .actions { flex: 0 0 auto; }
|
|
69
|
+
.feedback-panel .detail-scroll > .actions { margin: 0; }
|
|
70
|
+
|
|
71
|
+
/* Desktop triage: list on the left, selected feedback on the right. */
|
|
72
|
+
.ib-index, .ib-index .container { height: 100vh; height: 100dvh; }
|
|
73
|
+
.ib-index { overflow: hidden; }
|
|
74
|
+
.ib-index .container { max-width: 1280px; display: flex; flex-direction: column; padding-bottom: 16px; }
|
|
75
|
+
.ib-index .container > h1 { flex: 0 0 auto; margin-bottom: 14px; }
|
|
76
|
+
.triage-shell { flex: 1 1 auto; min-height: 0; display: grid; grid-template-columns: minmax(320px, 400px) 1fr;
|
|
77
|
+
gap: 16px; }
|
|
78
|
+
.triage-sidebar { min-width: 0; min-height: 0; display: flex; flex-direction: column;
|
|
79
|
+
background: var(--surface); border: 1px solid var(--border);
|
|
80
|
+
border-radius: 8px; overflow: hidden; }
|
|
81
|
+
.triage-sidebar .tabs { flex: 0 0 auto; margin: 12px 12px 0; padding: 0; background: transparent;
|
|
82
|
+
border: 0; border-radius: 0; gap: 6px; flex-wrap: nowrap; }
|
|
83
|
+
.triage-sidebar .tabs a { flex: 1 1 0; position: relative; display: flex; align-items: center;
|
|
84
|
+
justify-content: center; gap: 6px; min-height: 34px; padding: 5px 10px 8px;
|
|
85
|
+
border-radius: 8px; color: var(--muted); font-weight: 600; }
|
|
86
|
+
.triage-sidebar .tabs a.active { color: var(--text); border: 0; background: transparent; box-shadow: none; margin: 0; }
|
|
87
|
+
.triage-sidebar .tabs a.active::after { content: ""; position: absolute; left: 20px; right: 20px; bottom: 0;
|
|
88
|
+
height: 2px; border-radius: 999px; background: var(--accent); }
|
|
89
|
+
.triage-sidebar .tabs a:not(.active):hover { text-decoration: none; color: var(--text); background: var(--bg); }
|
|
90
|
+
.triage-sidebar .tabs .count { margin-left: 0; background: color-mix(in srgb, var(--muted) 14%, transparent); }
|
|
91
|
+
.triage-sidebar .tabs a.active .count { background: var(--border); }
|
|
92
|
+
.triage-sidebar .filters { flex: 0 0 auto; display: grid; grid-template-columns: minmax(0, 1fr) auto;
|
|
93
|
+
margin: 0; padding: 12px; border-bottom: 1px solid var(--border); }
|
|
94
|
+
.triage-sidebar .filters select { grid-column: 1 / -1; width: 100%; }
|
|
95
|
+
.triage-sidebar .filters input[type=search] { min-width: 0; max-width: none; }
|
|
96
|
+
.feedback-list { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
|
|
97
|
+
.feedback-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 12px; padding: 13px 14px;
|
|
98
|
+
color: var(--text); border-bottom: 1px solid var(--border); }
|
|
99
|
+
.feedback-row:hover { text-decoration: none; background: var(--bg); }
|
|
100
|
+
.feedback-row.active { background: color-mix(in srgb, var(--accent) 9%, var(--surface));
|
|
101
|
+
box-shadow: inset 3px 0 0 var(--accent); }
|
|
102
|
+
.feedback-main { min-width: 0; display: flex; flex-direction: column; gap: 5px; }
|
|
103
|
+
.feedback-topline, .feedback-meta { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
|
104
|
+
.feedback-message, .feedback-meta span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
105
|
+
.feedback-message { display: block; font-weight: 600; }
|
|
106
|
+
.feedback-time { margin-left: auto; white-space: nowrap; }
|
|
107
|
+
.attachment-count { align-self: start; white-space: nowrap; }
|
|
108
|
+
.triage-sidebar .pager { flex: 0 0 auto; margin: 0; padding: 10px 12px; border-top: 1px solid var(--border); }
|
|
109
|
+
.triage-detail { min-width: 0; min-height: 0; display: flex; flex-direction: column; overflow: hidden; }
|
|
110
|
+
.triage-detail .feedback-panel { flex: 1 1 auto; min-height: 0; }
|
|
111
|
+
.triage-detail .detail-scroll { flex: 1 1 auto; overflow-y: auto; padding-right: 2px; }
|
|
112
|
+
.triage-detail .actions { margin-bottom: 0; }
|
|
113
|
+
.empty-state { margin: auto; max-width: 320px; padding: 24px; text-align: center; }
|
|
114
|
+
.empty-state h2 { margin: 0 0 6px; font-size: 18px; }
|
|
115
|
+
.empty-state p { margin: 0; }
|
|
116
|
+
.mobile-back { display: none; }
|
|
117
|
+
|
|
118
|
+
/* Standalone show pages keep normal page scrolling. */
|
|
119
|
+
.ib-show { min-height: 100vh; overflow: auto; }
|
|
120
|
+
.ib-show .feedback-panel { gap: 0; }
|
|
121
|
+
.ib-show .detail-scroll { overflow: visible; }
|
|
122
|
+
@media (max-width: 760px) {
|
|
123
|
+
body { font-size: 14px; }
|
|
124
|
+
.container { padding: 14px 10px 24px; }
|
|
125
|
+
.ib-index, .ib-show { overflow-x: hidden; }
|
|
126
|
+
.feedback-panel, .detail-scroll, .card, dl, dd { min-width: 0; max-width: 100%; }
|
|
127
|
+
.feedback-panel .card { width: calc(100vw - 20px); }
|
|
128
|
+
.message, dd, .muted { max-width: calc(100vw - 54px); }
|
|
129
|
+
.ib-index { overflow-y: auto; overflow-x: hidden; }
|
|
130
|
+
.ib-index, .ib-index .container { min-height: 100vh; height: auto; }
|
|
131
|
+
.ib-index .container { padding-bottom: 10px; }
|
|
132
|
+
.triage-shell { display: block; min-height: calc(100vh - 62px); }
|
|
133
|
+
.triage-sidebar, .triage-detail { min-height: calc(100vh - 62px); }
|
|
134
|
+
.triage-shell.has-selected .triage-sidebar { display: none; }
|
|
135
|
+
.triage-shell:not(.has-selected) .triage-detail { display: none; }
|
|
136
|
+
.feedback-list { overflow: visible; }
|
|
137
|
+
.feedback-row { padding: 12px; }
|
|
138
|
+
.feedback-meta { display: none; }
|
|
139
|
+
.triage-detail .feedback-panel { min-height: calc(100vh - 64px); }
|
|
140
|
+
.mobile-back { display: block; flex: 0 0 auto; margin: 10px 10px 0; font-size: 13px; }
|
|
141
|
+
.panel-head h1 { font-size: 18px; }
|
|
142
|
+
dl { grid-template-columns: 1fr; gap: 3px; }
|
|
143
|
+
}
|
data/lib/ideasbugs/engine.rb
CHANGED
|
@@ -17,5 +17,14 @@ module Ideasbugs
|
|
|
17
17
|
extend Ideasbugs::HasFeedback
|
|
18
18
|
end
|
|
19
19
|
end
|
|
20
|
+
|
|
21
|
+
initializer 'ideasbugs.routing' do
|
|
22
|
+
ActionDispatch::Routing::Mapper.include(Module.new do
|
|
23
|
+
def mount_ideasbugs(at: Ideasbugs.config.mount_path, **options)
|
|
24
|
+
Ideasbugs.config.mount_path = at
|
|
25
|
+
mount Ideasbugs::Engine, at:, **options
|
|
26
|
+
end
|
|
27
|
+
end)
|
|
28
|
+
end
|
|
20
29
|
end
|
|
21
30
|
end
|
data/lib/ideasbugs/version.rb
CHANGED
data/lib/ideasbugs/widget.rb
CHANGED
|
@@ -13,6 +13,7 @@ module Ideasbugs
|
|
|
13
13
|
# host's asset namespace or precompiled output.
|
|
14
14
|
module Widget
|
|
15
15
|
SOURCE = File.expand_path('widget.js', __dir__)
|
|
16
|
+
DASHBOARD_STYLESHEET_SOURCE = File.expand_path('dashboard.css', __dir__)
|
|
16
17
|
|
|
17
18
|
# Right-to-left scripts, so the form renders mirrored for those locales.
|
|
18
19
|
# Matched on the language subtag, so region variants ("ar-EG") count too.
|
|
@@ -23,6 +24,10 @@ module Ideasbugs
|
|
|
23
24
|
@javascript ||= File.read(SOURCE)
|
|
24
25
|
end
|
|
25
26
|
|
|
27
|
+
def dashboard_stylesheet
|
|
28
|
+
@dashboard_stylesheet ||= File.read(DASHBOARD_STYLESHEET_SOURCE)
|
|
29
|
+
end
|
|
30
|
+
|
|
26
31
|
# Content fingerprint for the cache-busting script URL: a changed file
|
|
27
32
|
# is a changed URL, so no browser can ever run stale widget code —
|
|
28
33
|
# Safari has been caught ignoring must-revalidate on same-URL scripts.
|
|
@@ -30,6 +35,10 @@ module Ideasbugs
|
|
|
30
35
|
@fingerprint ||= Digest::MD5.hexdigest(javascript)
|
|
31
36
|
end
|
|
32
37
|
|
|
38
|
+
def dashboard_stylesheet_fingerprint
|
|
39
|
+
@dashboard_stylesheet_fingerprint ||= Digest::MD5.hexdigest(dashboard_stylesheet)
|
|
40
|
+
end
|
|
41
|
+
|
|
33
42
|
# The two <script> tags the helper renders.
|
|
34
43
|
#
|
|
35
44
|
# The config rides in a `type="application/json"` block: it is *data*,
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ideasbugs
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.7.
|
|
4
|
+
version: 0.7.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yaroslav Shmarov
|
|
@@ -45,6 +45,7 @@ files:
|
|
|
45
45
|
- app/helpers/ideasbugs/widget_helper.rb
|
|
46
46
|
- app/models/ideasbugs/application_record.rb
|
|
47
47
|
- app/models/ideasbugs/feedback.rb
|
|
48
|
+
- app/views/ideasbugs/feedbacks/_feedback_panel.html.erb
|
|
48
49
|
- app/views/ideasbugs/feedbacks/index.html.erb
|
|
49
50
|
- app/views/ideasbugs/feedbacks/show.html.erb
|
|
50
51
|
- app/views/layouts/ideasbugs/application.html.erb
|
|
@@ -82,6 +83,7 @@ files:
|
|
|
82
83
|
- lib/generators/ideasbugs/tenant/tenant_generator.rb
|
|
83
84
|
- lib/ideasbugs.rb
|
|
84
85
|
- lib/ideasbugs/configuration.rb
|
|
86
|
+
- lib/ideasbugs/dashboard.css
|
|
85
87
|
- lib/ideasbugs/engine.rb
|
|
86
88
|
- lib/ideasbugs/has_feedback.rb
|
|
87
89
|
- lib/ideasbugs/version.rb
|