ideasbugs 0.7.8 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +28 -0
- data/README.md +2 -0
- data/app/controllers/concerns/ideasbugs/request_context.rb +51 -0
- data/app/controllers/ideasbugs/application_controller.rb +9 -21
- data/app/controllers/ideasbugs/dashboard_controller.rb +30 -0
- data/app/controllers/ideasbugs/feedbacks_controller.rb +1 -108
- data/app/controllers/ideasbugs/screenshots_controller.rb +1 -3
- data/app/controllers/ideasbugs/submissions_controller.rb +100 -0
- data/app/views/ideasbugs/feedbacks/index.html.erb +87 -85
- data/app/views/ideasbugs/feedbacks/show.html.erb +7 -5
- data/app/views/ideasbugs/shared/_dashboard.html.erb +13 -0
- data/app/views/layouts/ideasbugs/application.html.erb +8 -5
- data/config/routes.rb +6 -2
- data/lib/generators/ideasbugs/install/install_generator.rb +2 -6
- data/lib/generators/ideasbugs/install/templates/create_ideasbugs_feedbacks.rb.tt +1 -1
- data/lib/generators/ideasbugs/install/templates/initializer.rb +8 -0
- data/lib/generators/ideasbugs/migration_helpers.rb +33 -0
- data/lib/generators/ideasbugs/tenant/tenant_generator.rb +2 -6
- data/lib/ideasbugs/configuration.rb +21 -1
- data/lib/ideasbugs/dashboard.css +160 -139
- data/lib/ideasbugs/version.rb +1 -1
- data/lib/ideasbugs.rb +7 -0
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 19b7014d62b42ad3b3a763edf8907a7e1e0d2a67ab7b5ae1001d1f3440cc0d67
|
|
4
|
+
data.tar.gz: 0f327125389c6ccc2d57f180b09d24607f383d03ac1d70ad13c9c250d3670842
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f1f27d97cda7c058c8d12c4498e30a57a9bc3899ce61aca61cb4cb5b542c4359b48e69d7a00c8867ddddc421cb561fb843039d6ac36bce4b164d9c140229e421
|
|
7
|
+
data.tar.gz: 9bca34a90ac4d1e4f15ddfb8263a0e8690aa61cb669f1519e83c43481e5e64c05133ec5317c4432b02484af66fdf2123f4574fbdb9710930bd1c0cdb94d3ed0f
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.0
|
|
4
|
+
|
|
5
|
+
- **`config.admin_layout` now works on its own.** The dashboard's stylesheet was
|
|
6
|
+
declared in the gem's layout, so replacing that layout dropped it and the
|
|
7
|
+
dashboard rendered unstyled. It moves into the views, so every layout gets it
|
|
8
|
+
with nothing asked of the host.
|
|
9
|
+
- **The dashboard stylesheet no longer claims selectors it does not own.** It
|
|
10
|
+
styled bare `*`, `body` and `a`, and its `.container`, `.card` and `.tabs` are
|
|
11
|
+
names other frameworks use too. Component rules now nest inside an
|
|
12
|
+
`.ib-dashboard` wrapper the views render, and every custom property is `--ib-`
|
|
13
|
+
prefixed — that collision ran both ways, so a host defining `--bg` recoloured
|
|
14
|
+
the dashboard just as easily.
|
|
15
|
+
- **Added `config.base_controller_class`.** Name the controller your own admin
|
|
16
|
+
inherits from and the dashboard adopts its layout, helpers, authentication and
|
|
17
|
+
request context. Default is unchanged.
|
|
18
|
+
- **`create` moved to `Ideasbugs::SubmissionsController`.** One controller served
|
|
19
|
+
both the widget's write endpoint and the triage actions, so
|
|
20
|
+
`base_controller_class` would have put staff authentication in front of every
|
|
21
|
+
bug report. `POST /feedbacks` now routes there; the URL is unchanged. If you
|
|
22
|
+
referenced `Ideasbugs::FeedbacksController#create`, that is the breaking change
|
|
23
|
+
in this release. The per-IP rate limiter moved with the action.
|
|
24
|
+
- **Migrations follow the host's `primary_key_type`,** the same
|
|
25
|
+
`Rails.configuration.generators` lookup Rails' own Active Storage migration
|
|
26
|
+
does. A uuid-keyed app has a uuid `active_storage_attachments.record_id`, so a
|
|
27
|
+
bigint table here could never hold a screenshot: `attach` raised
|
|
28
|
+
`NotNullViolation`. A host that set nothing gets an identical migration.
|
|
29
|
+
- A `BackboneTest` now fails the build on any of the above regressing.
|
|
30
|
+
|
|
3
31
|
## 0.7.8
|
|
4
32
|
|
|
5
33
|
- Adds `AGENTS.md`: install and integration instructions written for coding
|
data/README.md
CHANGED
|
@@ -102,6 +102,8 @@ Everything is optional — a fresh install works with zero config. In
|
|
|
102
102
|
| Option | Default | What it does |
|
|
103
103
|
| --------------------- | --------------------------- | --------------------------------------------------- |
|
|
104
104
|
| `authorize_admin` | development only | **Who can read the dashboard.** Override before deploying |
|
|
105
|
+
| `base_controller_class` | `ActionController::Base` | Controller the dashboard inherits — name your admin's and it adopts its layout, helpers and auth |
|
|
106
|
+
| `admin_layout` | the gem's own | Just the shell, if you don't want the whole controller |
|
|
105
107
|
| `enabled` | everyone | Who can send feedback. `false` hides the widget and rejects posts |
|
|
106
108
|
| `current_user` | `nil` | Attribute a submission to a user. Receives the request |
|
|
107
109
|
| `author_label` | the user's `email` | Short label stored and shown in the dashboard |
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ideasbugs
|
|
4
|
+
# Who is asking, which tenant they are in, and the gates that answer both.
|
|
5
|
+
#
|
|
6
|
+
# A concern rather than inherited behaviour because the engine has two
|
|
7
|
+
# controller roots: the public endpoints hang off ActionController::Base, and
|
|
8
|
+
# the dashboard hangs off whatever the host set as `base_controller_class`.
|
|
9
|
+
module RequestContext
|
|
10
|
+
extend ActiveSupport::Concern
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def ideasbugs_admin_layout
|
|
15
|
+
Ideasbugs.config.admin_layout
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def current_author
|
|
19
|
+
return @current_author if defined?(@current_author)
|
|
20
|
+
|
|
21
|
+
@current_author = Ideasbugs.config.current_user.call(request)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# The tenant for this request (nil = the single global board). Every read
|
|
25
|
+
# and write scopes to it, so a resolved-tenant admin only ever sees and
|
|
26
|
+
# writes their own tenant's feedback.
|
|
27
|
+
def current_tenant
|
|
28
|
+
return @current_tenant if defined?(@current_tenant)
|
|
29
|
+
|
|
30
|
+
@current_tenant = Ideasbugs.tenant(request)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def require_enabled
|
|
34
|
+
head :forbidden unless Ideasbugs.enabled?(request)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Server-side gate for the dashboard. Default: development only.
|
|
38
|
+
def require_admin
|
|
39
|
+
return if Ideasbugs.admin?(request)
|
|
40
|
+
|
|
41
|
+
render plain: 'Forbidden. Set Ideasbugs.config.authorize_admin to grant access.',
|
|
42
|
+
status: :forbidden
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Every dashboard query starts here, so an admin can only ever load,
|
|
46
|
+
# triage, or delete feedback in their own tenant — a cross-tenant id 404s.
|
|
47
|
+
def tenant_scope
|
|
48
|
+
Feedback.for_tenant(current_tenant)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -1,28 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Ideasbugs
|
|
4
|
+
# Root of the engine's PUBLIC surface: widget.js and the submission endpoint.
|
|
5
|
+
# These stay on a plain ActionController::Base deliberately — someone filing a
|
|
6
|
+
# bug report must not be routed through a host's admin controller, which would
|
|
7
|
+
# demand a staff session for the widget.
|
|
8
|
+
#
|
|
9
|
+
# The dashboard's root is DashboardController, and that is where
|
|
10
|
+
# `config.base_controller_class` applies.
|
|
4
11
|
class ApplicationController < ActionController::Base
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
private
|
|
8
|
-
|
|
9
|
-
def ideasbugs_admin_layout
|
|
10
|
-
Ideasbugs.config.admin_layout
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def current_author
|
|
14
|
-
return @current_author if defined?(@current_author)
|
|
12
|
+
include RequestContext
|
|
15
13
|
|
|
16
|
-
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
# The tenant for this request (nil = the single global board). Every read
|
|
20
|
-
# and write scopes to it, so a resolved-tenant admin only ever sees and
|
|
21
|
-
# writes their own tenant's feedback.
|
|
22
|
-
def current_tenant
|
|
23
|
-
return @current_tenant if defined?(@current_tenant)
|
|
24
|
-
|
|
25
|
-
@current_tenant = Ideasbugs.tenant(request)
|
|
26
|
-
end
|
|
14
|
+
protect_from_forgery with: :exception
|
|
27
15
|
end
|
|
28
16
|
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ideasbugs
|
|
4
|
+
# Root of the STAFF surface: the triage queue and the screenshot proxy.
|
|
5
|
+
#
|
|
6
|
+
# Inherits from `config.base_controller_class` — by default a plain
|
|
7
|
+
# ActionController::Base, which is why `authorize_admin` exists. Point it at
|
|
8
|
+
# the controller your own admin already inherits from and the dashboard picks
|
|
9
|
+
# up that stack wholesale: your layout, your helpers, your authentication, and
|
|
10
|
+
# whatever request context your before_actions establish.
|
|
11
|
+
#
|
|
12
|
+
# Only the dashboard hangs off it. The widget's endpoint stays on
|
|
13
|
+
# ApplicationController, so wiring an admin base controller here can never
|
|
14
|
+
# demand a staff session from someone filing a report.
|
|
15
|
+
class DashboardController < Ideasbugs.base_controller
|
|
16
|
+
include RequestContext
|
|
17
|
+
|
|
18
|
+
# A host base controller brings its own layout, and declaring one here would
|
|
19
|
+
# override it. So the gem only claims the layout when it owns the decision:
|
|
20
|
+
# no host base controller, or a host that named an `admin_layout` explicitly.
|
|
21
|
+
layout :ideasbugs_admin_layout unless superclass != ActionController::Base &&
|
|
22
|
+
Ideasbugs.config.admin_layout == Configuration::DEFAULT_ADMIN_LAYOUT
|
|
23
|
+
|
|
24
|
+
before_action :require_admin
|
|
25
|
+
|
|
26
|
+
# A host base controller has configured CSRF already; declaring it twice
|
|
27
|
+
# would run the check twice.
|
|
28
|
+
protect_from_forgery with: :exception if superclass == ActionController::Base
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -1,27 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Ideasbugs
|
|
4
|
-
class FeedbacksController <
|
|
4
|
+
class FeedbacksController < DashboardController
|
|
5
5
|
PER_PAGE = 50
|
|
6
6
|
|
|
7
|
-
layout :ideasbugs_admin_layout, except: :create
|
|
8
|
-
|
|
9
7
|
# The widget posts here; everything else is the triage dashboard.
|
|
10
|
-
before_action :require_enabled, only: :create
|
|
11
|
-
before_action :require_admin, except: :create
|
|
12
8
|
before_action :set_feedback, only: %i[show update destroy]
|
|
13
9
|
|
|
14
|
-
# Throttle the public endpoint per IP so one user or bot can't flood the
|
|
15
|
-
# table (each submission may carry megabytes of screenshots). Uses the
|
|
16
|
-
# rate limiter built into Rails 7.2+ (backed by Rails.cache); on Rails 7.1
|
|
17
|
-
# this is a no-op. Tune or disable via config.rate_limit — read once at
|
|
18
|
-
# boot, after the host's initializer.
|
|
19
|
-
if respond_to?(:rate_limit) && Ideasbugs.config.rate_limit
|
|
20
|
-
rate_limit(**Ideasbugs.config.rate_limit,
|
|
21
|
-
only: :create,
|
|
22
|
-
with: -> { render json: { errors: [t_error(:error_rate_limited)] }, status: :too_many_requests })
|
|
23
|
-
end
|
|
24
|
-
|
|
25
10
|
def index
|
|
26
11
|
@status = Feedback::STATUSES.include?(params[:status]) ? params[:status] : 'open'
|
|
27
12
|
@kind = Ideasbugs.config.kinds.map(&:to_s).include?(params[:kind]) ? params[:kind] : nil
|
|
@@ -46,23 +31,6 @@ module Ideasbugs
|
|
|
46
31
|
|
|
47
32
|
def show; end
|
|
48
33
|
|
|
49
|
-
def create
|
|
50
|
-
feedback = Feedback.new(feedback_params)
|
|
51
|
-
feedback.user_agent = request.user_agent
|
|
52
|
-
feedback.tenant = current_tenant
|
|
53
|
-
attribute_author(feedback)
|
|
54
|
-
|
|
55
|
-
error = attach_screenshots(feedback)
|
|
56
|
-
return render json: { errors: [error] }, status: :unprocessable_entity if error
|
|
57
|
-
|
|
58
|
-
if feedback.save
|
|
59
|
-
notify_host(feedback)
|
|
60
|
-
head :created
|
|
61
|
-
else
|
|
62
|
-
render json: { errors: feedback.errors.full_messages }, status: :unprocessable_entity
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
|
|
66
34
|
def update
|
|
67
35
|
@feedback.update!(params.require(:feedback).permit(:status))
|
|
68
36
|
redirect_back fallback_location: feedback_path(@feedback), status: :see_other
|
|
@@ -75,28 +43,10 @@ module Ideasbugs
|
|
|
75
43
|
|
|
76
44
|
private
|
|
77
45
|
|
|
78
|
-
def require_enabled
|
|
79
|
-
head :forbidden unless Ideasbugs.enabled?(request)
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
# Server-side gate for the dashboard. Default: development only.
|
|
83
|
-
def require_admin
|
|
84
|
-
return if Ideasbugs.admin?(request)
|
|
85
|
-
|
|
86
|
-
render plain: 'Forbidden. Set Ideasbugs.config.authorize_admin to grant access.',
|
|
87
|
-
status: :forbidden
|
|
88
|
-
end
|
|
89
|
-
|
|
90
46
|
def set_feedback
|
|
91
47
|
@feedback = tenant_scope.find(params[:id])
|
|
92
48
|
end
|
|
93
49
|
|
|
94
|
-
# Every dashboard query starts here, so an admin can only ever load,
|
|
95
|
-
# triage, or delete feedback in their own tenant — a cross-tenant id 404s.
|
|
96
|
-
def tenant_scope
|
|
97
|
-
Feedback.for_tenant(current_tenant)
|
|
98
|
-
end
|
|
99
|
-
|
|
100
50
|
# Case-insensitive match on the free-text columns. LOWER() keeps it
|
|
101
51
|
# portable across SQLite/PostgreSQL/MySQL, and the explicit ESCAPE makes
|
|
102
52
|
# the sanitized backslash escapes work on SQLite, which has no default
|
|
@@ -109,62 +59,5 @@ module Ideasbugs
|
|
|
109
59
|
q: pattern
|
|
110
60
|
)
|
|
111
61
|
end
|
|
112
|
-
|
|
113
|
-
# The host's hook must never turn a saved submission into a 500 — the
|
|
114
|
-
# feedback is in the database; notification failures are the host's logs'
|
|
115
|
-
# problem.
|
|
116
|
-
def notify_host(feedback)
|
|
117
|
-
Ideasbugs.config.on_submit.call(feedback)
|
|
118
|
-
rescue StandardError => e
|
|
119
|
-
Rails.logger.error("ideasbugs: on_submit hook raised #{e.class}: #{e.message}")
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
def feedback_params
|
|
123
|
-
params.require(:feedback).permit(:kind, :section, :message, :page_url)
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
def attribute_author(feedback)
|
|
127
|
-
author = current_author
|
|
128
|
-
return unless author
|
|
129
|
-
|
|
130
|
-
feedback.author_id = author.id.to_s if author.respond_to?(:id)
|
|
131
|
-
feedback.author_label = Ideasbugs.config.author_label.call(author)
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
# Validates and attaches uploads. Returns an error message, or nil when
|
|
135
|
-
# everything (including "no screenshots at all") is fine.
|
|
136
|
-
def attach_screenshots(feedback)
|
|
137
|
-
files = Array(params.dig(:feedback, :screenshots)).reject(&:blank?)
|
|
138
|
-
return nil if files.empty?
|
|
139
|
-
return t_error(:error_save) unless Ideasbugs.config.screenshots_enabled?
|
|
140
|
-
return t_error(:error_too_many, count: Ideasbugs.config.max_screenshots) if too_many?(files)
|
|
141
|
-
return t_error(:error_too_large, size: max_size_mb) if files.any? { |f| f.size > max_size }
|
|
142
|
-
return t_error(:error_save) unless files.all? { |f| f.content_type.to_s.start_with?('image/') }
|
|
143
|
-
|
|
144
|
-
feedback.screenshots.attach(files)
|
|
145
|
-
nil
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
def too_many?(files)
|
|
149
|
-
files.size > Ideasbugs.config.max_screenshots
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
def max_size
|
|
153
|
-
Ideasbugs.config.max_screenshot_size
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
def max_size_mb
|
|
157
|
-
max_size / (1024 * 1024)
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
def t_error(key, **args)
|
|
161
|
-
defaults = {
|
|
162
|
-
error_save: 'Could not send feedback. Please try again.',
|
|
163
|
-
error_too_many: 'Too many screenshots (max %{count}).',
|
|
164
|
-
error_too_large: 'A screenshot is too large (max %{size} MB).',
|
|
165
|
-
error_rate_limited: 'Too many submissions. Please wait a moment and try again.'
|
|
166
|
-
}
|
|
167
|
-
I18n.t(key, scope: :ideasbugs, default: defaults[key], **args)
|
|
168
|
-
end
|
|
169
62
|
end
|
|
170
63
|
end
|
|
@@ -6,9 +6,7 @@ module Ideasbugs
|
|
|
6
6
|
# screenshots can contain anything a user's screen showed, so they must never
|
|
7
7
|
# be reachable without passing the same gate as the dashboard — regardless of
|
|
8
8
|
# how the host app configures (or doesn't configure) blob access.
|
|
9
|
-
class ScreenshotsController <
|
|
10
|
-
before_action :require_admin
|
|
11
|
-
|
|
9
|
+
class ScreenshotsController < DashboardController
|
|
12
10
|
def show
|
|
13
11
|
screenshot = Feedback.for_tenant(current_tenant)
|
|
14
12
|
.find(params[:feedback_id]).screenshots.find(params[:id])
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ideasbugs
|
|
4
|
+
# The widget's write endpoint: POST /feedbacks.
|
|
5
|
+
#
|
|
6
|
+
# Public, so it stays on ApplicationController and never inherits a host's
|
|
7
|
+
# admin base controller — someone filing a bug report must not be asked for a
|
|
8
|
+
# staff session. The triage actions live in FeedbacksController, which does
|
|
9
|
+
# inherit it.
|
|
10
|
+
class SubmissionsController < ApplicationController
|
|
11
|
+
before_action :require_enabled
|
|
12
|
+
|
|
13
|
+
# Throttle the public endpoint per IP so one user or bot can't flood the
|
|
14
|
+
# table (each submission may carry megabytes of screenshots). Uses the
|
|
15
|
+
# rate limiter built into Rails 7.2+ (backed by Rails.cache); on Rails 7.1
|
|
16
|
+
# this is a no-op. Tune or disable via config.rate_limit — read once at
|
|
17
|
+
# boot, after the host's initializer.
|
|
18
|
+
if respond_to?(:rate_limit) && Ideasbugs.config.rate_limit
|
|
19
|
+
rate_limit(**Ideasbugs.config.rate_limit,
|
|
20
|
+
only: :create,
|
|
21
|
+
with: -> { render json: { errors: [t_error(:error_rate_limited)] }, status: :too_many_requests })
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def create
|
|
25
|
+
feedback = Feedback.new(feedback_params)
|
|
26
|
+
feedback.user_agent = request.user_agent
|
|
27
|
+
feedback.tenant = current_tenant
|
|
28
|
+
attribute_author(feedback)
|
|
29
|
+
|
|
30
|
+
error = attach_screenshots(feedback)
|
|
31
|
+
return render json: { errors: [error] }, status: :unprocessable_entity if error
|
|
32
|
+
|
|
33
|
+
if feedback.save
|
|
34
|
+
notify_host(feedback)
|
|
35
|
+
head :created
|
|
36
|
+
else
|
|
37
|
+
render json: { errors: feedback.errors.full_messages }, status: :unprocessable_entity
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
# The host's hook must never turn a saved submission into a 500 — the
|
|
44
|
+
# feedback is in the database; notification failures are the host's logs'
|
|
45
|
+
# problem.
|
|
46
|
+
def notify_host(feedback)
|
|
47
|
+
Ideasbugs.config.on_submit.call(feedback)
|
|
48
|
+
rescue StandardError => e
|
|
49
|
+
Rails.logger.error("ideasbugs: on_submit hook raised #{e.class}: #{e.message}")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def feedback_params
|
|
53
|
+
params.require(:feedback).permit(:kind, :section, :message, :page_url)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def attribute_author(feedback)
|
|
57
|
+
author = current_author
|
|
58
|
+
return unless author
|
|
59
|
+
|
|
60
|
+
feedback.author_id = author.id.to_s if author.respond_to?(:id)
|
|
61
|
+
feedback.author_label = Ideasbugs.config.author_label.call(author)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Validates and attaches uploads. Returns an error message, or nil when
|
|
65
|
+
# everything (including "no screenshots at all") is fine.
|
|
66
|
+
def attach_screenshots(feedback)
|
|
67
|
+
files = Array(params.dig(:feedback, :screenshots)).reject(&:blank?)
|
|
68
|
+
return nil if files.empty?
|
|
69
|
+
return t_error(:error_save) unless Ideasbugs.config.screenshots_enabled?
|
|
70
|
+
return t_error(:error_too_many, count: Ideasbugs.config.max_screenshots) if too_many?(files)
|
|
71
|
+
return t_error(:error_too_large, size: max_size_mb) if files.any? { |f| f.size > max_size }
|
|
72
|
+
return t_error(:error_save) unless files.all? { |f| f.content_type.to_s.start_with?('image/') }
|
|
73
|
+
|
|
74
|
+
feedback.screenshots.attach(files)
|
|
75
|
+
nil
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def too_many?(files)
|
|
79
|
+
files.size > Ideasbugs.config.max_screenshots
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def max_size_mb
|
|
83
|
+
max_size / (1024 * 1024)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def max_size
|
|
87
|
+
Ideasbugs.config.max_screenshot_size
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def t_error(key, **args)
|
|
91
|
+
defaults = {
|
|
92
|
+
error_save: 'Could not send feedback. Please try again.',
|
|
93
|
+
error_too_many: 'Too many screenshots (max %{count}).',
|
|
94
|
+
error_too_large: 'A screenshot is too large (max %{size} MB).',
|
|
95
|
+
error_rate_limited: 'Too many submissions. Please wait a moment and try again.'
|
|
96
|
+
}
|
|
97
|
+
I18n.t(key, scope: :ideasbugs, default: defaults[key], **args)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -1,99 +1,101 @@
|
|
|
1
1
|
<% content_for :body_class, 'ib-index' %>
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<%= render layout: 'ideasbugs/shared/dashboard' do %>
|
|
4
|
+
<h1><%= t('ideasbugs.dashboard.title', default: 'IdeasBugs') %></h1>
|
|
4
5
|
|
|
5
|
-
<div class="triage-shell <%= 'has-selected' if @selected_feedback %>">
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
6
|
+
<div class="triage-shell <%= 'has-selected' if @selected_feedback %>">
|
|
7
|
+
<aside class="triage-sidebar" aria-label="<%= t('ideasbugs.dashboard.feedback', default: 'Feedback') %>">
|
|
8
|
+
<div class="tabs">
|
|
9
|
+
<% Ideasbugs::Feedback::STATUSES.each do |status| %>
|
|
10
|
+
<%= link_to feedbacks_path(status: status, kind: @kind, q: @query),
|
|
11
|
+
class: ('active' if status == @status) do %>
|
|
12
|
+
<%= t("ideasbugs.statuses.#{status}", default: status.humanize) %>
|
|
13
|
+
<span class="count"><%= @counts.fetch(status, 0) %></span>
|
|
14
|
+
<% end %>
|
|
13
15
|
<% end %>
|
|
14
|
-
|
|
15
|
-
</div>
|
|
16
|
+
</div>
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
18
|
+
<form class="filters" method="get">
|
|
19
|
+
<input type="hidden" name="status" value="<%= @status %>">
|
|
20
|
+
<select name="kind" onchange="this.form.submit()">
|
|
21
|
+
<option value=""><%= t('ideasbugs.dashboard.all_kinds', default: 'All types') %></option>
|
|
22
|
+
<% Ideasbugs.config.kinds.each do |kind| %>
|
|
23
|
+
<option value="<%= kind %>" <%= 'selected' if kind.to_s == @kind %>>
|
|
24
|
+
<%= t("ideasbugs.kinds.#{kind}", default: kind.to_s.humanize) %>
|
|
25
|
+
</option>
|
|
26
|
+
<% end %>
|
|
27
|
+
</select>
|
|
28
|
+
<input type="search" name="q" value="<%= @query %>"
|
|
29
|
+
placeholder="<%= t('ideasbugs.dashboard.search_placeholder', default: 'Search message, author, section…') %>"
|
|
30
|
+
aria-label="<%= t('ideasbugs.dashboard.search', default: 'Search') %>">
|
|
31
|
+
<button><%= t('ideasbugs.dashboard.search', default: 'Search') %></button>
|
|
32
|
+
</form>
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
34
|
+
<div class="feedback-list">
|
|
35
|
+
<% if @feedbacks.any? %>
|
|
36
|
+
<% @feedbacks.each do |feedback| %>
|
|
37
|
+
<%= link_to feedbacks_path(status: @status, kind: @kind, q: @query, page: @page,
|
|
38
|
+
feedback_id: feedback.id),
|
|
39
|
+
class: ['feedback-row', ('active' if @selected_feedback&.id == feedback.id)].compact do %>
|
|
40
|
+
<span class="feedback-main">
|
|
41
|
+
<span class="feedback-topline">
|
|
42
|
+
<span class="badge kind-<%= feedback.kind.to_s.parameterize %>">
|
|
43
|
+
<%= t("ideasbugs.kinds.#{feedback.kind}", default: feedback.kind.humanize) %>
|
|
44
|
+
</span>
|
|
45
|
+
<span class="badge status-<%= feedback.status %>">
|
|
46
|
+
<%= t("ideasbugs.statuses.#{feedback.status}", default: feedback.status.humanize) %>
|
|
47
|
+
</span>
|
|
48
|
+
<span class="muted feedback-time" title="<%= feedback.created_at %>">
|
|
49
|
+
<%= time_ago_in_words(feedback.created_at) %>
|
|
50
|
+
</span>
|
|
43
51
|
</span>
|
|
44
|
-
<span class="
|
|
45
|
-
|
|
52
|
+
<span class="feedback-message"><%= truncate(feedback.message, length: 120) %></span>
|
|
53
|
+
<span class="muted feedback-meta">
|
|
54
|
+
<span>#<%= feedback.id %></span>
|
|
55
|
+
<% if @show_section && feedback.section.present? %>
|
|
56
|
+
<span class="feedback-section"><%= feedback.section %></span>
|
|
57
|
+
<% end %>
|
|
58
|
+
<% if feedback.author_label.present? %>
|
|
59
|
+
<span><%= feedback.author_label %></span>
|
|
60
|
+
<% end %>
|
|
46
61
|
</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 %>
|
|
60
62
|
</span>
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
<% if feedback.screenshots? %>
|
|
64
|
+
<span class="muted attachment-count">📎 <%= feedback.screenshots.count %></span>
|
|
65
|
+
<% end %>
|
|
64
66
|
<% end %>
|
|
65
67
|
<% end %>
|
|
68
|
+
<% else %>
|
|
69
|
+
<div class="empty"><%= t('ideasbugs.dashboard.empty', default: 'Nothing here yet.') %></div>
|
|
66
70
|
<% end %>
|
|
67
|
-
|
|
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>
|
|
71
|
+
</div>
|
|
83
72
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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>
|
|
73
|
+
<div class="pager">
|
|
74
|
+
<% if @page > 1 %>
|
|
75
|
+
<%= link_to t('ideasbugs.dashboard.newer', default: '← Newer'),
|
|
76
|
+
feedbacks_path(status: @status, kind: @kind, q: @query, page: @page - 1) %>
|
|
77
|
+
<% end %>
|
|
78
|
+
<% if @more %>
|
|
79
|
+
<%= link_to t('ideasbugs.dashboard.older', default: 'Older →'),
|
|
80
|
+
feedbacks_path(status: @status, kind: @kind, q: @query, page: @page + 1) %>
|
|
81
|
+
<% end %>
|
|
96
82
|
</div>
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
83
|
+
</aside>
|
|
84
|
+
|
|
85
|
+
<main class="triage-detail" aria-label="<%= t('ideasbugs.dashboard.current_feedback', default: 'Current feedback') %>">
|
|
86
|
+
<% if @selected_feedback %>
|
|
87
|
+
<p class="mobile-back">
|
|
88
|
+
<%= link_to t('ideasbugs.dashboard.back', default: '← All feedback'),
|
|
89
|
+
feedbacks_path(status: @status, kind: @kind, q: @query, page: @page) %>
|
|
90
|
+
</p>
|
|
91
|
+
<%= render 'feedback_panel', feedback: @selected_feedback %>
|
|
92
|
+
<% else %>
|
|
93
|
+
<div class="empty-state">
|
|
94
|
+
<h2><%= t('ideasbugs.dashboard.select_feedback', default: 'Select feedback') %></h2>
|
|
95
|
+
<p class="muted"><%= t('ideasbugs.dashboard.select_feedback_hint',
|
|
96
|
+
default: 'Open an item from the list to triage it without leaving the queue.') %></p>
|
|
97
|
+
</div>
|
|
98
|
+
<% end %>
|
|
99
|
+
</main>
|
|
100
|
+
</div>
|
|
101
|
+
<% end %>
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
<% content_for :body_class, 'ib-show' %>
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
<%= render layout: 'ideasbugs/shared/dashboard' do %>
|
|
4
|
+
<div class="breadcrumb">
|
|
5
|
+
<%= link_to t('ideasbugs.dashboard.title', default: 'IdeasBugs'), root_path %>
|
|
6
|
+
/ #<%= @feedback.id %>
|
|
7
|
+
</div>
|
|
7
8
|
|
|
8
|
-
<%= render 'feedback_panel', feedback: @feedback %>
|
|
9
|
+
<%= render 'feedback_panel', feedback: @feedback %>
|
|
10
|
+
<% end %>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<%# The dashboard's own shell, rendered by the views rather than the layout.
|
|
2
|
+
That is deliberate: `config.admin_layout` and `config.base_controller_class`
|
|
3
|
+
both let a host replace the layout, and a stylesheet declared in a layout the
|
|
4
|
+
host replaces simply vanishes — the dashboard then renders unstyled.
|
|
5
|
+
Declaring it here means every layout works, host or gem.
|
|
6
|
+
|
|
7
|
+
The `ib-dashboard` wrapper is what scopes dashboard.css (see the comment at
|
|
8
|
+
the top of that file), so it has to enclose the content, not precede it. %>
|
|
9
|
+
<%= stylesheet_link_tag "#{dashboard_stylesheet_path}?v=#{Ideasbugs::Widget.dashboard_stylesheet_fingerprint}",
|
|
10
|
+
'data-turbo-track': 'reload' %>
|
|
11
|
+
<div class="ib-dashboard">
|
|
12
|
+
<%= yield %>
|
|
13
|
+
</div>
|
|
@@ -5,12 +5,15 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<%= csrf_meta_tags %>
|
|
7
7
|
<%= csp_meta_tag %>
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
<%# The dashboard's stylesheet is declared by the views
|
|
9
|
+
(ideasbugs/shared/_dashboard), not here, so it survives a host replacing
|
|
10
|
+
this layout via config.admin_layout or config.base_controller_class. %>
|
|
10
11
|
</head>
|
|
11
|
-
<body class="<%= content_for?(:body_class) ? yield(:body_class) : '' %>">
|
|
12
|
-
<div class="
|
|
13
|
-
|
|
12
|
+
<body class="ib-page <%= content_for?(:body_class) ? yield(:body_class) : '' %>">
|
|
13
|
+
<div class="ib-dashboard">
|
|
14
|
+
<div class="container">
|
|
15
|
+
<%= yield %>
|
|
16
|
+
</div>
|
|
14
17
|
</div>
|
|
15
18
|
</body>
|
|
16
19
|
</html>
|
data/config/routes.rb
CHANGED
|
@@ -5,8 +5,12 @@ Ideasbugs::Engine.routes.draw do
|
|
|
5
5
|
get 'widget.js', to: 'widgets#show', as: :widget
|
|
6
6
|
get 'dashboard.css', to: 'widgets#dashboard_stylesheet', as: :dashboard_stylesheet
|
|
7
7
|
|
|
8
|
-
#
|
|
9
|
-
|
|
8
|
+
# POST goes to SubmissionsController, not to this resource: the widget's write
|
|
9
|
+
# endpoint is public and the rest is staff-only, and only the staff half
|
|
10
|
+
# inherits config.base_controller_class. Sharing one controller would put a
|
|
11
|
+
# host's admin authentication in front of someone filing a report.
|
|
12
|
+
post 'feedbacks', to: 'submissions#create', as: :submissions
|
|
13
|
+
resources :feedbacks, only: %i[index show update destroy] do
|
|
10
14
|
# Screenshots stream through the dashboard's own gate, never via public
|
|
11
15
|
# Active Storage blob URLs.
|
|
12
16
|
resources :screenshots, only: :show
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
require 'rails/generators'
|
|
4
4
|
require 'rails/generators/active_record'
|
|
5
|
+
require_relative '../migration_helpers'
|
|
5
6
|
|
|
6
7
|
module Ideasbugs
|
|
7
8
|
module Generators
|
|
8
9
|
class InstallGenerator < Rails::Generators::Base
|
|
9
10
|
include ActiveRecord::Generators::Migration
|
|
11
|
+
include MigrationHelpers
|
|
10
12
|
|
|
11
13
|
source_root File.expand_path('templates', __dir__)
|
|
12
14
|
|
|
@@ -31,12 +33,6 @@ module Ideasbugs
|
|
|
31
33
|
say 'Optional: run `bin/rails ideasbugs:seed_demo` for sample feedback.'
|
|
32
34
|
say "Browse submissions at /feedback (development only until you set config.authorize_admin).\n"
|
|
33
35
|
end
|
|
34
|
-
|
|
35
|
-
private
|
|
36
|
-
|
|
37
|
-
def migration_version
|
|
38
|
-
"[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
|
|
39
|
-
end
|
|
40
36
|
end
|
|
41
37
|
end
|
|
42
38
|
end
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
class CreateIdeasbugsFeedbacks < ActiveRecord::Migration<%= migration_version %>
|
|
4
4
|
def change
|
|
5
|
-
create_table :ideasbugs_feedbacks do |t|
|
|
5
|
+
create_table :ideasbugs_feedbacks<%= primary_key_type_option %> do |t|
|
|
6
6
|
t.string :kind, null: false, default: 'other'
|
|
7
7
|
t.string :section
|
|
8
8
|
t.text :message, null: false
|
|
@@ -11,6 +11,14 @@ Ideasbugs.configure do |config|
|
|
|
11
11
|
#
|
|
12
12
|
# Render the dashboard inside your app's admin layout. Default: the gem's
|
|
13
13
|
# standalone dashboard layout.
|
|
14
|
+
# Two ways to put the dashboard inside an admin you already have.
|
|
15
|
+
#
|
|
16
|
+
# The whole stack — your layout, helpers, authentication and any request
|
|
17
|
+
# context your before_actions set up. Only the dashboard inherits it; the
|
|
18
|
+
# widget's endpoint stays public, so this cannot gate a bug report.
|
|
19
|
+
# config.base_controller_class = "Admin::BaseController"
|
|
20
|
+
#
|
|
21
|
+
# Or the layout alone:
|
|
14
22
|
# config.admin_layout = "admin/application"
|
|
15
23
|
|
|
16
24
|
# Attribute feedback to a user (optional). Return an object responding to
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ideasbugs
|
|
4
|
+
module Generators
|
|
5
|
+
# Shared bits every migration-writing generator needs.
|
|
6
|
+
module MigrationHelpers
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def migration_version
|
|
10
|
+
"[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Follow the host's own key type instead of forcing bigint. An app that
|
|
14
|
+
# keys its models with uuids has a uuid
|
|
15
|
+
# `active_storage_attachments.record_id`, so a bigint table here could
|
|
16
|
+
# never take a screenshot: the foreign key has nowhere to point and
|
|
17
|
+
# `attach` raises NotNullViolation.
|
|
18
|
+
#
|
|
19
|
+
# Same lookup Rails' own Active Storage, Action Text and Action Mailbox
|
|
20
|
+
# migrations do, so a host that set it once gets consistent tables from
|
|
21
|
+
# all of them.
|
|
22
|
+
#
|
|
23
|
+
# Rendered as a `create_table` option rather than a bare value, because a
|
|
24
|
+
# template is expanded at generate time: emitting the method name would
|
|
25
|
+
# put `id: primary_key_type` in the migration, where nothing defines it.
|
|
26
|
+
def primary_key_type_option
|
|
27
|
+
config = Rails.configuration.generators
|
|
28
|
+
type = config.options[config.orm][:primary_key_type]
|
|
29
|
+
type ? ", id: :#{type}" : ''
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'rails/generators'
|
|
4
4
|
require 'rails/generators/active_record'
|
|
5
|
+
require_relative '../migration_helpers'
|
|
5
6
|
|
|
6
7
|
module Ideasbugs
|
|
7
8
|
module Generators
|
|
@@ -14,6 +15,7 @@ module Ideasbugs
|
|
|
14
15
|
# bin/rails generate ideasbugs:tenant && bin/rails db:migrate
|
|
15
16
|
class TenantGenerator < Rails::Generators::Base
|
|
16
17
|
include ActiveRecord::Generators::Migration
|
|
18
|
+
include MigrationHelpers
|
|
17
19
|
|
|
18
20
|
source_root File.expand_path('templates', __dir__)
|
|
19
21
|
|
|
@@ -28,12 +30,6 @@ module Ideasbugs
|
|
|
28
30
|
say "\ntenant column queued. Run `rails db:migrate`, then set", :green
|
|
29
31
|
say 'config.tenant in config/initializers/ideasbugs.rb to scope per tenant.'
|
|
30
32
|
end
|
|
31
|
-
|
|
32
|
-
private
|
|
33
|
-
|
|
34
|
-
def migration_version
|
|
35
|
-
"[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
|
|
36
|
-
end
|
|
37
33
|
end
|
|
38
34
|
end
|
|
39
35
|
end
|
|
@@ -5,6 +5,10 @@ module Ideasbugs
|
|
|
5
5
|
# works with zero configuration; the hooks below let an app decide who can
|
|
6
6
|
# send feedback, who can read it, and how submissions are attributed.
|
|
7
7
|
class Configuration
|
|
8
|
+
# The gem's own dashboard layout. Compared against, so DashboardController
|
|
9
|
+
# can tell "the host left this alone" from "the host chose this".
|
|
10
|
+
DEFAULT_ADMIN_LAYOUT = 'ideasbugs/application'
|
|
11
|
+
|
|
8
12
|
# Per-request gate for the widget and the submission endpoint. Return false
|
|
9
13
|
# to hide the widget and reject submissions for this request. Receives the
|
|
10
14
|
# request. Defaults to everyone — feedback collection is meant for real
|
|
@@ -20,6 +24,21 @@ module Ideasbugs
|
|
|
20
24
|
# inside your app's admin shell, e.g. "admin/application".
|
|
21
25
|
attr_accessor :admin_layout
|
|
22
26
|
|
|
27
|
+
# The controller the DASHBOARD inherits from, as a String so it resolves
|
|
28
|
+
# lazily rather than at config time. Default: a plain
|
|
29
|
+
# 'ActionController::Base', where `authorize_admin` is the only gate.
|
|
30
|
+
#
|
|
31
|
+
# Name the controller your own admin already inherits from and the dashboard
|
|
32
|
+
# adopts that whole stack — layout, helpers, authentication, and any request
|
|
33
|
+
# context your before_actions set up. `admin_layout` covers only the layout,
|
|
34
|
+
# which leaves a host layout calling its own helpers to raise NameError under
|
|
35
|
+
# the engine's isolated namespace.
|
|
36
|
+
#
|
|
37
|
+
# Only the dashboard uses it. The widget's endpoint stays on the engine's own
|
|
38
|
+
# public controller, so an admin base controller here can never demand a
|
|
39
|
+
# staff session from someone filing a report.
|
|
40
|
+
attr_accessor :base_controller_class
|
|
41
|
+
|
|
23
42
|
# Resolve the current user for attribution (optional). Return an object
|
|
24
43
|
# responding to #id, or nil. Receives the request.
|
|
25
44
|
attr_accessor :current_user
|
|
@@ -83,7 +102,8 @@ module Ideasbugs
|
|
|
83
102
|
def initialize
|
|
84
103
|
@enabled = ->(_request) { true }
|
|
85
104
|
@authorize_admin = ->(_request) { Rails.env.development? }
|
|
86
|
-
@admin_layout =
|
|
105
|
+
@admin_layout = DEFAULT_ADMIN_LAYOUT
|
|
106
|
+
@base_controller_class = 'ActionController::Base'
|
|
87
107
|
@current_user = ->(_request) {}
|
|
88
108
|
@tenant = ->(_request) {}
|
|
89
109
|
@author_label = ->(user) { user.respond_to?(:email) ? user.email : user&.to_s }
|
data/lib/ideasbugs/dashboard.css
CHANGED
|
@@ -1,143 +1,164 @@
|
|
|
1
|
-
|
|
1
|
+
/* Dashboard styles.
|
|
2
|
+
*
|
|
3
|
+
* Three layers, and the split is what lets a host replace the layout:
|
|
4
|
+
*
|
|
5
|
+
* :root custom properties, all `--ib-` prefixed so they can
|
|
6
|
+
* neither overwrite a host's nor be overwritten by them.
|
|
7
|
+
* .ib-page the page frame (was bare html/body). Only the gem's own
|
|
8
|
+
* layout puts this class on <body>, so inside a host admin
|
|
9
|
+
* these rules simply do not apply.
|
|
10
|
+
* .ib-dashboard everything else, nested. Names like .container, .card
|
|
11
|
+
* and .tabs are confined to the dashboard's own markup, so
|
|
12
|
+
* loading this file inside a host cannot restyle its chrome.
|
|
13
|
+
*
|
|
14
|
+
* Nothing here styles a bare `body`, `a`, `table` or `*` at the top level.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
:root {
|
|
2
19
|
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
|
-
|
|
9
|
-
|
|
10
|
-
--bg: #111418; --surface: #1a1f26; --text: #e6e8ea; --muted: #9aa2ab;
|
|
11
|
-
--border: #2a313a; --accent: #3b82f6;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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; }
|
|
20
|
+
--ib-bg: #f6f7f9; --ib-surface: #fff; --ib-text: #1c2024; --ib-muted: #6b7280;
|
|
21
|
+
--ib-border: #e5e7eb; --ib-accent: #2563eb; --ib-accent-text: #fff;
|
|
22
|
+
--ib-bug: #dc2626; --ib-feature: #16a34a; --ib-other: #6b7280;
|
|
23
|
+
--ib-open: #2563eb; --in_review: #d97706; --ib-resolved: #16a34a;
|
|
24
|
+
}
|
|
25
|
+
@media (prefers-color-scheme: dark) {
|
|
26
|
+
:root {
|
|
27
|
+
--ib-bg: #111418; --ib-surface: #1a1f26; --ib-text: #e6e8ea; --ib-muted: #9aa2ab;
|
|
28
|
+
--ib-border: #2a313a; --ib-accent: #3b82f6;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
@media (max-width: 760px) {
|
|
32
|
+
.ib-page { font-size: 14px; }
|
|
33
|
+
.ib-index, .ib-show { overflow-x: hidden; }
|
|
34
|
+
.ib-index { overflow-y: auto; overflow-x: hidden; }
|
|
35
|
+
.ib-index, .ib-index .container { min-height: 100vh; height: auto; }
|
|
36
|
+
.ib-index .container { padding-bottom: 10px; }
|
|
37
|
+
.ib-dashboard {
|
|
38
|
+
& .container { padding: 14px 10px 24px; }
|
|
39
|
+
& .feedback-panel,
|
|
40
|
+
& .detail-scroll,
|
|
41
|
+
& .card,
|
|
42
|
+
& dl,
|
|
43
|
+
& dd { min-width: 0; max-width: 100%; }
|
|
44
|
+
& .feedback-panel .card { width: calc(100vw - 20px); }
|
|
45
|
+
& .message,
|
|
46
|
+
& dd,
|
|
47
|
+
& .muted { max-width: calc(100vw - 54px); }
|
|
48
|
+
& .triage-shell { display: block; min-height: calc(100vh - 62px); }
|
|
49
|
+
& .triage-sidebar,
|
|
50
|
+
& .triage-detail { min-height: calc(100vh - 62px); }
|
|
51
|
+
& .triage-shell.has-selected .triage-sidebar { display: none; }
|
|
52
|
+
& .triage-shell:not(.has-selected) .triage-detail { display: none; }
|
|
53
|
+
& .feedback-list { overflow: visible; }
|
|
54
|
+
& .feedback-row { padding: 12px; }
|
|
55
|
+
& .feedback-meta { display: none; }
|
|
56
|
+
& .triage-detail .feedback-panel { min-height: calc(100vh - 64px); }
|
|
57
|
+
& .mobile-back { display: block; flex: 0 0 auto; margin: 10px 10px 0; font-size: 13px; }
|
|
58
|
+
& .panel-head h1 { font-size: 18px; }
|
|
59
|
+
& dl { grid-template-columns: 1fr; gap: 3px; }
|
|
60
|
+
}
|
|
61
|
+
}
|
|
70
62
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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; }
|
|
63
|
+
/* Page frame — the gem's own layout only. */
|
|
64
|
+
.ib-page { margin: 0; background: var(--ib-bg); color: var(--ib-text); font: 15px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif; }
|
|
65
|
+
.ib-index { overflow: hidden; }
|
|
66
|
+
.ib-index .container { max-width: 1280px; display: flex; flex-direction: column; padding-bottom: 16px; }
|
|
67
|
+
.ib-index .container > h1 { flex: 0 0 auto; margin-bottom: 14px; }
|
|
68
|
+
.ib-show .feedback-panel { gap: 0; }
|
|
69
|
+
.ib-show .detail-scroll { overflow: visible; }
|
|
117
70
|
|
|
118
|
-
|
|
71
|
+
/* Dashboard components — any layout. */
|
|
72
|
+
.ib-dashboard {
|
|
73
|
+
& * { box-sizing: border-box; }
|
|
74
|
+
& a { color: var(--ib-accent); text-decoration: none; }
|
|
75
|
+
& a:hover { text-decoration: underline; }
|
|
76
|
+
& .container { max-width: 960px; margin: 0 auto; padding: 24px 16px 64px; }
|
|
77
|
+
& h1 { font-size: 22px; margin: 0 0 16px; }
|
|
78
|
+
& .tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--ib-border); margin-bottom: 16px; flex-wrap: wrap; }
|
|
79
|
+
& .tabs a { padding: 8px 14px; border-radius: 8px 8px 0 0; color: var(--ib-muted); }
|
|
80
|
+
& .tabs a.active { color: var(--ib-text); font-weight: 600; border: 1px solid var(--ib-border); border-bottom: 2px solid var(--ib-surface); background: var(--ib-surface); margin-bottom: -1px; }
|
|
81
|
+
& .tabs a:hover { text-decoration: none; color: var(--ib-text); }
|
|
82
|
+
& .count { display: inline-block; min-width: 20px; padding: 0 6px; margin-left: 4px; border-radius: 999px; background: var(--ib-border); font-size: 12px; text-align: center; }
|
|
83
|
+
& .filters { margin-bottom: 16px; display: flex; gap: 8px; align-items: center; }
|
|
84
|
+
& .filters select,
|
|
85
|
+
& .filters input[type=search] { padding: 6px 8px; border: 1px solid var(--ib-border); border-radius: 8px; background: var(--ib-surface); color: var(--ib-text); font: inherit; }
|
|
86
|
+
& .filters input[type=search] { flex: 1; max-width: 320px; }
|
|
87
|
+
& .card { background: var(--ib-surface); border: 1px solid var(--ib-border); border-radius: 12px; overflow: hidden; }
|
|
88
|
+
& .card.pad { padding: 16px; }
|
|
89
|
+
& table { width: 100%; border-collapse: collapse; }
|
|
90
|
+
& th,
|
|
91
|
+
& td { padding: 10px 14px; text-align: left; vertical-align: top; }
|
|
92
|
+
& th { font-size: 12px; text-transform: uppercase; letter-spacing: .04em; color: var(--ib-muted); border-bottom: 1px solid var(--ib-border); }
|
|
93
|
+
& tr + tr td { border-top: 1px solid var(--ib-border); }
|
|
94
|
+
& .badge { display: inline-block; padding: 2px 10px; border-radius: 999px; font-size: 12px; font-weight: 600; color: #fff; white-space: nowrap; }
|
|
95
|
+
& .badge.kind-bug { background: var(--ib-bug); }
|
|
96
|
+
& .badge.kind-feature { background: var(--ib-feature); }
|
|
97
|
+
& .badge.kind-other,
|
|
98
|
+
& .badge.kind-default { background: var(--ib-other); }
|
|
99
|
+
& .badge.status-open { background: var(--ib-open); }
|
|
100
|
+
& .badge.status-in_review { background: var(--in_review); }
|
|
101
|
+
& .badge.status-resolved { background: var(--ib-resolved); }
|
|
102
|
+
& .muted { color: var(--ib-muted); font-size: 13px; }
|
|
103
|
+
& .pager { margin-top: 16px; display: flex; gap: 16px; }
|
|
104
|
+
& .actions { display: flex; gap: 8px; flex-wrap: wrap; margin: 16px 0; }
|
|
105
|
+
& .actions form { display: inline; }
|
|
106
|
+
& button,
|
|
107
|
+
& .button { padding: 8px 14px; border: 1px solid var(--ib-border); border-radius: 8px; cursor: pointer; background: var(--ib-surface); color: var(--ib-text); font: inherit; }
|
|
108
|
+
& button.primary { background: var(--ib-accent); border-color: var(--ib-accent); color: var(--ib-accent-text); }
|
|
109
|
+
& button.danger { color: var(--ib-bug); }
|
|
110
|
+
& .message { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
111
|
+
& dl { display: grid; grid-template-columns: max-content 1fr; gap: 6px 16px; margin: 0; }
|
|
112
|
+
& dt { color: var(--ib-muted); }
|
|
113
|
+
& dd { margin: 0; overflow-wrap: anywhere; }
|
|
114
|
+
& .shots { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 8px; }
|
|
115
|
+
& .shots img { max-width: 280px; max-height: 200px; border: 1px solid var(--ib-border); border-radius: 8px; }
|
|
116
|
+
& .empty { padding: 48px 16px; text-align: center; color: var(--ib-muted); }
|
|
117
|
+
& .breadcrumb { margin-bottom: 12px; font-size: 13px; }
|
|
118
|
+
& .case-id { color: var(--ib-muted); font-weight: 400; font-size: 15px; }
|
|
119
|
+
& .panel-head { flex: 0 0 auto; }
|
|
120
|
+
& .panel-head h1 { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }
|
|
121
|
+
& .detail-scroll { display: flex; flex-direction: column; gap: 12px; min-height: 0; }
|
|
122
|
+
& .feedback-panel { min-width: 0; display: flex; flex-direction: column; }
|
|
123
|
+
& .feedback-panel .actions { flex: 0 0 auto; }
|
|
124
|
+
& .feedback-panel .detail-scroll > .actions { margin: 0; }
|
|
125
|
+
& /* Desktop triage: list on the left,
|
|
126
|
+
& selected feedback on the right. */
|
|
127
|
+
.ib-index,
|
|
128
|
+
& .ib-index .container { height: 100vh; height: 100dvh; }
|
|
129
|
+
& .triage-shell { flex: 1 1 auto; min-height: 0; display: grid; grid-template-columns: minmax(320px, 400px) 1fr; gap: 16px; }
|
|
130
|
+
& .triage-sidebar { min-width: 0; min-height: 0; display: flex; flex-direction: column; background: var(--ib-surface); border: 1px solid var(--ib-border); border-radius: 8px; overflow: hidden; }
|
|
131
|
+
& .triage-sidebar .tabs { flex: 0 0 auto; margin: 12px 12px 0; padding: 0; background: transparent; border: 0; border-radius: 0; gap: 6px; flex-wrap: nowrap; }
|
|
132
|
+
& .triage-sidebar .tabs a { flex: 1 1 0; position: relative; display: flex; align-items: center; justify-content: center; gap: 6px; min-height: 34px; padding: 5px 10px 8px; border-radius: 8px; color: var(--ib-muted); font-weight: 600; }
|
|
133
|
+
& .triage-sidebar .tabs a.active { color: var(--ib-text); border: 0; background: transparent; box-shadow: none; margin: 0; }
|
|
134
|
+
& .triage-sidebar .tabs a.active::after { content: ""; position: absolute; left: 20px; right: 20px; bottom: 0; height: 2px; border-radius: 999px; background: var(--ib-accent); }
|
|
135
|
+
& .triage-sidebar .tabs a:not(.active):hover { text-decoration: none; color: var(--ib-text); background: var(--ib-bg); }
|
|
136
|
+
& .triage-sidebar .tabs .count { margin-left: 0; background: color-mix(in srgb, var(--ib-muted) 14%, transparent); }
|
|
137
|
+
& .triage-sidebar .tabs a.active .count { background: var(--ib-border); }
|
|
138
|
+
& .triage-sidebar .filters { flex: 0 0 auto; display: grid; grid-template-columns: minmax(0, 1fr) auto; margin: 0; padding: 12px; border-bottom: 1px solid var(--ib-border); }
|
|
139
|
+
& .triage-sidebar .filters select { grid-column: 1 / -1; width: 100%; }
|
|
140
|
+
& .triage-sidebar .filters input[type=search] { min-width: 0; max-width: none; }
|
|
141
|
+
& .feedback-list { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
|
|
142
|
+
& .feedback-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 12px; padding: 13px 14px; color: var(--ib-text); border-bottom: 1px solid var(--ib-border); }
|
|
143
|
+
& .feedback-row:hover { text-decoration: none; background: var(--ib-bg); }
|
|
144
|
+
& .feedback-row.active { background: color-mix(in srgb, var(--ib-accent) 9%, var(--ib-surface)); box-shadow: inset 3px 0 0 var(--ib-accent); }
|
|
145
|
+
& .feedback-main { min-width: 0; display: flex; flex-direction: column; gap: 5px; }
|
|
146
|
+
& .feedback-topline,
|
|
147
|
+
& .feedback-meta { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
|
148
|
+
& .feedback-message,
|
|
149
|
+
& .feedback-meta span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
150
|
+
& .feedback-message { display: block; font-weight: 600; }
|
|
151
|
+
& .feedback-time { margin-left: auto; white-space: nowrap; }
|
|
152
|
+
& .attachment-count { align-self: start; white-space: nowrap; }
|
|
153
|
+
& .triage-sidebar .pager { flex: 0 0 auto; margin: 0; padding: 10px 12px; border-top: 1px solid var(--ib-border); }
|
|
154
|
+
& .triage-detail { min-width: 0; min-height: 0; display: flex; flex-direction: column; overflow: hidden; }
|
|
155
|
+
& .triage-detail .feedback-panel { flex: 1 1 auto; min-height: 0; }
|
|
156
|
+
& .triage-detail .detail-scroll { flex: 1 1 auto; overflow-y: auto; padding-right: 2px; }
|
|
157
|
+
& .triage-detail .actions { margin-bottom: 0; }
|
|
158
|
+
& .empty-state { margin: auto; max-width: 320px; padding: 24px; text-align: center; }
|
|
159
|
+
& .empty-state h2 { margin: 0 0 6px; font-size: 18px; }
|
|
160
|
+
& .empty-state p { margin: 0; }
|
|
161
|
+
& .mobile-back { display: none; }
|
|
162
|
+
& /* Standalone show pages keep normal page scrolling. */
|
|
119
163
|
.ib-show { min-height: 100vh; overflow: auto; }
|
|
120
|
-
|
|
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
|
-
}
|
|
164
|
+
}
|
data/lib/ideasbugs/version.rb
CHANGED
data/lib/ideasbugs.rb
CHANGED
|
@@ -29,6 +29,13 @@ module Ideasbugs
|
|
|
29
29
|
|
|
30
30
|
# Can this request browse and triage feedback? Checked by every dashboard
|
|
31
31
|
# action.
|
|
32
|
+
# The class Ideasbugs::DashboardController inherits from. Resolved on every
|
|
33
|
+
# call rather than memoized, so a host that reassigns base_controller_class
|
|
34
|
+
# in a reloadable initializer is not pinned to a stale, unloaded constant.
|
|
35
|
+
def base_controller
|
|
36
|
+
config.base_controller_class.to_s.constantize
|
|
37
|
+
end
|
|
38
|
+
|
|
32
39
|
def admin?(request)
|
|
33
40
|
!!config.authorize_admin.call(request)
|
|
34
41
|
end
|
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.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yaroslav Shmarov
|
|
@@ -39,9 +39,12 @@ files:
|
|
|
39
39
|
- MIT-LICENSE
|
|
40
40
|
- README.md
|
|
41
41
|
- Rakefile
|
|
42
|
+
- app/controllers/concerns/ideasbugs/request_context.rb
|
|
42
43
|
- app/controllers/ideasbugs/application_controller.rb
|
|
44
|
+
- app/controllers/ideasbugs/dashboard_controller.rb
|
|
43
45
|
- app/controllers/ideasbugs/feedbacks_controller.rb
|
|
44
46
|
- app/controllers/ideasbugs/screenshots_controller.rb
|
|
47
|
+
- app/controllers/ideasbugs/submissions_controller.rb
|
|
45
48
|
- app/controllers/ideasbugs/widgets_controller.rb
|
|
46
49
|
- app/helpers/ideasbugs/widget_helper.rb
|
|
47
50
|
- app/models/ideasbugs/application_record.rb
|
|
@@ -49,6 +52,7 @@ files:
|
|
|
49
52
|
- app/views/ideasbugs/feedbacks/_feedback_panel.html.erb
|
|
50
53
|
- app/views/ideasbugs/feedbacks/index.html.erb
|
|
51
54
|
- app/views/ideasbugs/feedbacks/show.html.erb
|
|
55
|
+
- app/views/ideasbugs/shared/_dashboard.html.erb
|
|
52
56
|
- app/views/layouts/ideasbugs/application.html.erb
|
|
53
57
|
- config/locales/ideasbugs.ar.yml
|
|
54
58
|
- config/locales/ideasbugs.bg.yml
|
|
@@ -80,6 +84,7 @@ files:
|
|
|
80
84
|
- lib/generators/ideasbugs/install/install_generator.rb
|
|
81
85
|
- lib/generators/ideasbugs/install/templates/create_ideasbugs_feedbacks.rb.tt
|
|
82
86
|
- lib/generators/ideasbugs/install/templates/initializer.rb
|
|
87
|
+
- lib/generators/ideasbugs/migration_helpers.rb
|
|
83
88
|
- lib/generators/ideasbugs/tenant/templates/add_tenant_to_ideasbugs_feedbacks.rb.tt
|
|
84
89
|
- lib/generators/ideasbugs/tenant/tenant_generator.rb
|
|
85
90
|
- lib/ideasbugs.rb
|