testimonials 0.7.10 → 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.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +4 -1
  3. data/CHANGELOG.md +45 -0
  4. data/README.md +2 -0
  5. data/app/controllers/concerns/testimonials/request_context.rb +75 -0
  6. data/app/controllers/testimonials/application_controller.rb +10 -56
  7. data/app/controllers/testimonials/dashboard_controller.rb +40 -0
  8. data/app/controllers/testimonials/nps_responses_controller.rb +1 -4
  9. data/app/controllers/testimonials/submissions_controller.rb +169 -0
  10. data/app/controllers/testimonials/testimonials_controller.rb +6 -166
  11. data/app/views/layouts/testimonials/application.html.erb +5 -8
  12. data/app/views/testimonials/nps_responses/index.html.erb +154 -152
  13. data/app/views/testimonials/nps_responses/show.html.erb +7 -5
  14. data/app/views/testimonials/shared/_dashboard.html.erb +18 -0
  15. data/app/views/testimonials/testimonials/index.html.erb +110 -108
  16. data/app/views/testimonials/testimonials/show.html.erb +7 -5
  17. data/config/routes.rb +18 -5
  18. data/lib/generators/testimonials/install/install_generator.rb +2 -6
  19. data/lib/generators/testimonials/install/templates/create_testimonials_tables.rb.tt +3 -3
  20. data/lib/generators/testimonials/install/templates/initializer.rb.tt +10 -2
  21. data/lib/generators/testimonials/migration_helpers.rb +35 -0
  22. data/lib/generators/testimonials/nps/nps_generator.rb +2 -6
  23. data/lib/generators/testimonials/nps/templates/create_testimonials_nps_responses.rb.tt +1 -1
  24. data/lib/generators/testimonials/prompt_events/prompt_events_generator.rb +2 -6
  25. data/lib/generators/testimonials/prompt_events/templates/create_testimonials_prompt_events.rb.tt +1 -1
  26. data/lib/generators/testimonials/tenant/tenant_generator.rb +2 -6
  27. data/lib/testimonials/configuration.rb +24 -1
  28. data/lib/testimonials/dashboard.css +278 -221
  29. data/lib/testimonials/version.rb +1 -1
  30. data/lib/testimonials.rb +8 -0
  31. metadata +6 -1
@@ -1,124 +1,126 @@
1
1
  <% content_for :body_class, 'tm-index' %>
2
2
 
3
- <div class="page-head">
4
- <h1><%= t('testimonials.dashboard.title', default: 'Testimonials') %></h1>
5
- <%# The shareable public form — drop this link into an email or DM. It lives
6
- on the page, not in the nav, so it survives a host's admin_layout. %>
7
- <% if Testimonials.config.public_collection %>
8
- <a class="button share-link" href="<%= collection_url %>" target="_blank" rel="noopener"
9
- title="<%= collection_url %>">
10
- 🔗 <%= t('testimonials.dashboard.collection_page', default: 'Ask for testimonials') %>
11
- </a>
12
- <% end %>
13
- </div>
3
+ <%= render layout: 'testimonials/shared/dashboard' do %>
4
+ <div class="page-head">
5
+ <h1><%= t('testimonials.dashboard.title', default: 'Testimonials') %></h1>
6
+ <%# The shareable public form drop this link into an email or DM. It lives
7
+ on the page, not in the nav, so it survives a host's admin_layout. %>
8
+ <% if Testimonials.config.public_collection %>
9
+ <a class="button share-link" href="<%= collection_url %>" target="_blank" rel="noopener"
10
+ title="<%= collection_url %>">
11
+ 🔗 <%= t('testimonials.dashboard.collection_page', default: 'Ask for testimonials') %>
12
+ </a>
13
+ <% end %>
14
+ </div>
14
15
 
15
- <div class="dashboard-shell <%= 'has-selected' if @selected_testimonial %>">
16
- <aside class="dashboard-sidebar" aria-label="<%= t('testimonials.dashboard.testimonials', default: 'Testimonials') %>">
17
- <div class="tabs">
18
- <% Testimonials::Testimonial::STATUSES.each do |status| %>
19
- <%= link_to testimonials_path(status: status, kind: @kind, q: @query),
20
- class: ('active' if status == @status) do %>
21
- <%= t("testimonials.statuses.#{status}", default: status.humanize) %>
22
- <span class="count"><%= @counts.fetch(status, 0) %></span>
16
+ <div class="dashboard-shell <%= 'has-selected' if @selected_testimonial %>">
17
+ <aside class="dashboard-sidebar" aria-label="<%= t('testimonials.dashboard.testimonials', default: 'Testimonials') %>">
18
+ <div class="tabs">
19
+ <% Testimonials::Testimonial::STATUSES.each do |status| %>
20
+ <%= link_to testimonials_path(status: status, kind: @kind, q: @query),
21
+ class: ('active' if status == @status) do %>
22
+ <%= t("testimonials.statuses.#{status}", default: status.humanize) %>
23
+ <span class="count"><%= @counts.fetch(status, 0) %></span>
24
+ <% end %>
23
25
  <% end %>
24
- <% end %>
25
- </div>
26
+ </div>
26
27
 
27
- <form class="filters" method="get">
28
- <input type="hidden" name="status" value="<%= @status %>">
29
- <select name="kind" data-autosubmit="true">
30
- <option value=""><%= t('testimonials.dashboard.all_kinds', default: 'All kinds') %></option>
31
- <% Testimonials::Testimonial::KINDS.each do |kind| %>
32
- <option value="<%= kind %>" <%= 'selected' if kind == @kind %>>
33
- <%= t("testimonials.kinds.#{kind}", default: kind.humanize) %>
34
- </option>
35
- <% end %>
36
- </select>
37
- <input type="search" name="q" value="<%= @query %>"
38
- placeholder="<%= t('testimonials.dashboard.search_placeholder', default: 'Search text, name, email…') %>"
39
- aria-label="<%= t('testimonials.dashboard.search', default: 'Search') %>">
40
- <button><%= t('testimonials.dashboard.search', default: 'Search') %></button>
41
- </form>
28
+ <form class="filters" method="get">
29
+ <input type="hidden" name="status" value="<%= @status %>">
30
+ <select name="kind" data-autosubmit="true">
31
+ <option value=""><%= t('testimonials.dashboard.all_kinds', default: 'All kinds') %></option>
32
+ <% Testimonials::Testimonial::KINDS.each do |kind| %>
33
+ <option value="<%= kind %>" <%= 'selected' if kind == @kind %>>
34
+ <%= t("testimonials.kinds.#{kind}", default: kind.humanize) %>
35
+ </option>
36
+ <% end %>
37
+ </select>
38
+ <input type="search" name="q" value="<%= @query %>"
39
+ placeholder="<%= t('testimonials.dashboard.search_placeholder', default: 'Search text, name, email…') %>"
40
+ aria-label="<%= t('testimonials.dashboard.search', default: 'Search') %>">
41
+ <button><%= t('testimonials.dashboard.search', default: 'Search') %></button>
42
+ </form>
42
43
 
43
- <div class="record-list">
44
- <% if @testimonials.any? %>
45
- <% @testimonials.each do |testimonial| %>
46
- <%= link_to testimonials_path(status: @status, kind: @kind, q: @query, page: @page,
47
- testimonial_id: testimonial.id),
48
- class: ['record-row testimonial-row',
49
- ('active' if @selected_testimonial&.id == testimonial.id)].compact do %>
50
- <span class="record-main">
51
- <span class="record-topline">
52
- <% if testimonial.rating %>
53
- <span class="stars"><%= '★' * testimonial.rating %><span class="off"><%= '★' * (5 - testimonial.rating) %></span></span>
54
- <% else %>
55
- <span class="badge locale"><%= t("testimonials.kinds.#{testimonial.kind}", default: testimonial.kind.humanize) %></span>
56
- <% end %>
57
- <span class="badge status-<%= testimonial.status %>">
58
- <%= t("testimonials.statuses.#{testimonial.status}", default: testimonial.status.humanize) %>
44
+ <div class="record-list">
45
+ <% if @testimonials.any? %>
46
+ <% @testimonials.each do |testimonial| %>
47
+ <%= link_to testimonials_path(status: @status, kind: @kind, q: @query, page: @page,
48
+ testimonial_id: testimonial.id),
49
+ class: ['record-row testimonial-row',
50
+ ('active' if @selected_testimonial&.id == testimonial.id)].compact do %>
51
+ <span class="record-main">
52
+ <span class="record-topline">
53
+ <% if testimonial.rating %>
54
+ <span class="stars"><%= '★' * testimonial.rating %><span class="off"><%= '★' * (5 - testimonial.rating) %></span></span>
55
+ <% else %>
56
+ <span class="badge locale"><%= t("testimonials.kinds.#{testimonial.kind}", default: testimonial.kind.humanize) %></span>
57
+ <% end %>
58
+ <span class="badge status-<%= testimonial.status %>">
59
+ <%= t("testimonials.statuses.#{testimonial.status}", default: testimonial.status.humanize) %>
60
+ </span>
61
+ <span class="muted record-time" title="<%= testimonial.created_at %>">
62
+ <%= time_ago_in_words(testimonial.created_at) %>
63
+ </span>
59
64
  </span>
60
- <span class="muted record-time" title="<%= testimonial.created_at %>">
61
- <%= time_ago_in_words(testimonial.created_at) %>
65
+ <span class="record-copy">
66
+ <% if testimonial.body.present? %>
67
+ <%= truncate(testimonial.body, length: 120) %>
68
+ <% else %>
69
+ <em><%= t('testimonials.kinds.video', default: 'Video') %></em>
70
+ <% end %>
71
+ </span>
72
+ <span class="muted record-meta">
73
+ <span>#<%= testimonial.id %></span>
74
+ <% if testimonial.display_name.present? %>
75
+ <span><%= testimonial.display_name %></span>
76
+ <% end %>
77
+ <% if testimonial.title_company.present? %>
78
+ <span><%= testimonial.title_company %></span>
79
+ <% end %>
62
80
  </span>
63
81
  </span>
64
- <span class="record-copy">
65
- <% if testimonial.body.present? %>
66
- <%= truncate(testimonial.body, length: 120) %>
67
- <% else %>
68
- <em><%= t('testimonials.kinds.video', default: 'Video') %></em>
69
- <% end %>
70
- </span>
71
- <span class="muted record-meta">
72
- <span>#<%= testimonial.id %></span>
73
- <% if testimonial.display_name.present? %>
74
- <span><%= testimonial.display_name %></span>
82
+ <span class="record-side">
83
+ <% if testimonial.video_attached? %>
84
+ <span class="muted">🎥</span>
75
85
  <% end %>
76
- <% if testimonial.title_company.present? %>
77
- <span><%= testimonial.title_company %></span>
86
+ <% if testimonial.featured? %>
87
+ <span class="muted">⭐</span>
78
88
  <% end %>
89
+ <span class="muted"><%= testimonial.consent_given? ? '✓' : '—' %></span>
79
90
  </span>
80
- </span>
81
- <span class="record-side">
82
- <% if testimonial.video_attached? %>
83
- <span class="muted">🎥</span>
84
- <% end %>
85
- <% if testimonial.featured? %>
86
- <span class="muted">⭐</span>
87
- <% end %>
88
- <span class="muted"><%= testimonial.consent_given? ? '✓' : '—' %></span>
89
- </span>
91
+ <% end %>
90
92
  <% end %>
93
+ <% else %>
94
+ <div class="empty"><%= t('testimonials.dashboard.empty', default: 'Nothing here yet.') %></div>
91
95
  <% end %>
92
- <% else %>
93
- <div class="empty"><%= t('testimonials.dashboard.empty', default: 'Nothing here yet.') %></div>
94
- <% end %>
95
- </div>
96
-
97
- <div class="pager">
98
- <% if @page > 1 %>
99
- <%= link_to t('testimonials.dashboard.newer', default: '← Newer'),
100
- testimonials_path(status: @status, kind: @kind, q: @query, page: @page - 1) %>
101
- <% end %>
102
- <% if @more %>
103
- <%= link_to t('testimonials.dashboard.older', default: 'Older →'),
104
- testimonials_path(status: @status, kind: @kind, q: @query, page: @page + 1) %>
105
- <% end %>
106
- </div>
107
- </aside>
96
+ </div>
108
97
 
109
- <main class="dashboard-detail" aria-label="<%= t('testimonials.dashboard.current_testimonial', default: 'Current testimonial') %>">
110
- <% if @selected_testimonial %>
111
- <p class="mobile-back">
112
- <%= link_to t('testimonials.dashboard.back', default: '← All testimonials'),
113
- testimonials_path(status: @status, kind: @kind, q: @query, page: @page) %>
114
- </p>
115
- <%= render 'testimonial_panel', testimonial: @selected_testimonial %>
116
- <% else %>
117
- <div class="empty-state">
118
- <h2><%= t('testimonials.dashboard.select_testimonial', default: 'Select a testimonial') %></h2>
119
- <p class="muted"><%= t('testimonials.dashboard.select_testimonial_hint',
120
- default: 'Open a review from the list to approve, feature, or tune the best line.') %></p>
98
+ <div class="pager">
99
+ <% if @page > 1 %>
100
+ <%= link_to t('testimonials.dashboard.newer', default: '← Newer'),
101
+ testimonials_path(status: @status, kind: @kind, q: @query, page: @page - 1) %>
102
+ <% end %>
103
+ <% if @more %>
104
+ <%= link_to t('testimonials.dashboard.older', default: 'Older →'),
105
+ testimonials_path(status: @status, kind: @kind, q: @query, page: @page + 1) %>
106
+ <% end %>
121
107
  </div>
122
- <% end %>
123
- </main>
124
- </div>
108
+ </aside>
109
+
110
+ <main class="dashboard-detail" aria-label="<%= t('testimonials.dashboard.current_testimonial', default: 'Current testimonial') %>">
111
+ <% if @selected_testimonial %>
112
+ <p class="mobile-back">
113
+ <%= link_to t('testimonials.dashboard.back', default: '← All testimonials'),
114
+ testimonials_path(status: @status, kind: @kind, q: @query, page: @page) %>
115
+ </p>
116
+ <%= render 'testimonial_panel', testimonial: @selected_testimonial %>
117
+ <% else %>
118
+ <div class="empty-state">
119
+ <h2><%= t('testimonials.dashboard.select_testimonial', default: 'Select a testimonial') %></h2>
120
+ <p class="muted"><%= t('testimonials.dashboard.select_testimonial_hint',
121
+ default: 'Open a review from the list to approve, feature, or tune the best line.') %></p>
122
+ </div>
123
+ <% end %>
124
+ </main>
125
+ </div>
126
+ <% end %>
@@ -1,8 +1,10 @@
1
1
  <% content_for :body_class, 'tm-show' %>
2
2
 
3
- <div class="breadcrumb">
4
- <%= link_to t('testimonials.dashboard.title', default: 'Testimonials'), root_path %>
5
- / #<%= @testimonial.id %>
6
- </div>
3
+ <%= render layout: 'testimonials/shared/dashboard' do %>
4
+ <div class="breadcrumb">
5
+ <%= link_to t('testimonials.dashboard.title', default: 'Testimonials'), root_path %>
6
+ / #<%= @testimonial.id %>
7
+ </div>
7
8
 
8
- <%= render 'testimonial_panel', testimonial: @testimonial %>
9
+ <%= render 'testimonial_panel', testimonial: @testimonial %>
10
+ <% end %>
data/config/routes.rb CHANGED
@@ -22,14 +22,27 @@ Testimonials::Engine.routes.draw do
22
22
  end
23
23
 
24
24
  # Media by testimonial id, gated (admin, author, or public_api + publishable).
25
- get ':id/video', to: 'media#video', as: :testimonial_video, constraints: { id: /\d+/ }
26
- get ':id/poster', to: 'media#poster', as: :testimonial_poster, constraints: { id: /\d+/ }
27
- get ':id/avatar', to: 'media#avatar', as: :testimonial_avatar, constraints: { id: /\d+/ }
25
+ get ':id/video', to: 'media#video', as: :testimonial_video
26
+ get ':id/poster', to: 'media#poster', as: :testimonial_poster
27
+ get ':id/avatar', to: 'media#avatar', as: :testimonial_avatar
28
28
 
29
29
  # Flat, human URLs: the mount path IS the resource. /testimonials is the
30
30
  # dashboard, POST /testimonials the widget endpoint, /testimonials/2 a testimonial.
31
- resources :testimonials, path: '', only: %i[create index show update destroy],
32
- constraints: { id: /\d+/ }
31
+ #
32
+ # No `id: /\d+/` constraint: it made these routes bigint-only, which forced
33
+ # the tables to be bigint too, which meant a uuid-keyed host could never
34
+ # attach a video or avatar (its active_storage_attachments.record_id is a
35
+ # uuid column). The constraint was never load-bearing — every fixed-name
36
+ # route above is declared first, so ordering already does the disambiguating,
37
+ # and an id that matches no record still 404s, just via RecordNotFound rather
38
+ # than a routing error.
39
+ #
40
+ # POST goes to SubmissionsController, not to this resource: the widget's write
41
+ # endpoint is public and the rest is staff-only, and only the staff half
42
+ # inherits config.base_controller_class. Sharing one controller would put a
43
+ # host's admin authentication in front of a member leaving a review.
44
+ post '', to: 'submissions#create', as: :submissions
45
+ resources :testimonials, path: '', only: %i[index show update destroy]
33
46
 
34
47
  root to: 'testimonials#index'
35
48
  end
@@ -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 Testimonials
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
 
@@ -59,12 +61,6 @@ module Testimonials
59
61
  'widget — open it from your own button. Add it later with ' \
60
62
  '`bin/rails generate testimonials:prompt_events`.', :yellow
61
63
  end
62
-
63
- private
64
-
65
- def migration_version
66
- "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
67
- end
68
64
  end
69
65
  end
70
66
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  class CreateTestimonialsTables < ActiveRecord::Migration<%= migration_version %>
4
4
  def change
5
- create_table :testimonials_testimonials do |t|
5
+ create_table :testimonials_testimonials<%= primary_key_type_option %> do |t|
6
6
  t.string :kind, null: false, default: 'text'
7
7
  t.text :body
8
8
  t.integer :rating
@@ -28,7 +28,7 @@ class CreateTestimonialsTables < ActiveRecord::Migration<%= migration_version %>
28
28
  add_index :testimonials_testimonials, %i[tenant status consent_given]
29
29
  <% unless options[:skip_nps] -%>
30
30
 
31
- create_table :testimonials_nps_responses do |t|
31
+ create_table :testimonials_nps_responses<%= primary_key_type_option %> do |t|
32
32
  t.integer :score, null: false
33
33
  t.text :comment
34
34
  t.string :author_id
@@ -45,7 +45,7 @@ class CreateTestimonialsTables < ActiveRecord::Migration<%= migration_version %>
45
45
  <% end -%>
46
46
  <% unless options[:skip_prompt_events] -%>
47
47
 
48
- create_table :testimonials_prompt_events do |t|
48
+ create_table :testimonials_prompt_events<%= primary_key_type_option %> do |t|
49
49
  t.string :kind, null: false
50
50
  t.string :action, null: false
51
51
  t.string :author_id
@@ -13,9 +13,17 @@ Testimonials.configure do |config|
13
13
  # only — override before deploying.
14
14
  # config.authorize_admin = ->(request) { request.env["warden"]&.user&.admin? }
15
15
  #
16
- # Render the dashboard inside your app's admin layout. Default: the gem's
17
- # standalone dashboard layout.
16
+ # Two ways to put the dashboard inside an admin you already have.
17
+ #
18
+ # The layout only — the gem's controllers, your shell:
18
19
  # config.admin_layout = "admin/application"
20
+ #
21
+ # Or the whole stack. Name the controller your own admin inherits from and
22
+ # the dashboard picks up its layout, helpers, authentication and any request
23
+ # context its before_actions set up. Only the dashboard inherits it; the
24
+ # widget's endpoints stay public, so this cannot ask a member leaving a
25
+ # review for a staff session.
26
+ # config.base_controller_class = "Admin::BaseController"
19
27
 
20
28
  # Attribute submissions to a user (optional). Return an object responding
21
29
  # to #id, or nil. Receives the request.
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Testimonials
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 sets this, and its
15
+ # `active_storage_attachments.record_id` is then a uuid column — bigint
16
+ # tables here could never take a video or avatar attachment, because a
17
+ # uuid foreign key has nowhere to point.
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
+ # A host with no setting gets no option at all, so the migration reads
27
+ # exactly as it always did.
28
+ def primary_key_type_option
29
+ config = Rails.configuration.generators
30
+ type = config.options[config.orm][:primary_key_type]
31
+ type ? ", id: :#{type}" : ''
32
+ end
33
+ end
34
+ end
35
+ 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 Testimonials
7
8
  module Generators
@@ -12,6 +13,7 @@ module Testimonials
12
13
  # bin/rails generate testimonials:nps && bin/rails db:migrate
13
14
  class NpsGenerator < Rails::Generators::Base
14
15
  include ActiveRecord::Generators::Migration
16
+ include MigrationHelpers
15
17
 
16
18
  source_root File.expand_path('templates', __dir__)
17
19
 
@@ -26,12 +28,6 @@ module Testimonials
26
28
  say "\nNPS table queued. Run `rails db:migrate`, then set", :green
27
29
  say 'config.nps = true in config/initializers/testimonials.rb to turn the flow on.'
28
30
  end
29
-
30
- private
31
-
32
- def migration_version
33
- "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
34
- end
35
31
  end
36
32
  end
37
33
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  class CreateTestimonialsNpsResponses < ActiveRecord::Migration<%= migration_version %>
4
4
  def change
5
- create_table :testimonials_nps_responses do |t|
5
+ create_table :testimonials_nps_responses<%= primary_key_type_option %> do |t|
6
6
  t.integer :score, null: false
7
7
  t.text :comment
8
8
  t.string :author_id
@@ -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 Testimonials
7
8
  module Generators
@@ -13,6 +14,7 @@ module Testimonials
13
14
  # bin/rails generate testimonials:prompt_events && bin/rails db:migrate
14
15
  class PromptEventsGenerator < Rails::Generators::Base
15
16
  include ActiveRecord::Generators::Migration
17
+ include MigrationHelpers
16
18
 
17
19
  source_root File.expand_path('templates', __dir__)
18
20
 
@@ -28,12 +30,6 @@ module Testimonials
28
30
  say 'config.prompt_events = true in config/initializers/testimonials.rb to let'
29
31
  say 'testimonial_prompt! auto-open the widget again.'
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
@@ -2,7 +2,7 @@
2
2
 
3
3
  class CreateTestimonialsPromptEvents < ActiveRecord::Migration<%= migration_version %>
4
4
  def change
5
- create_table :testimonials_prompt_events do |t|
5
+ create_table :testimonials_prompt_events<%= primary_key_type_option %> do |t|
6
6
  t.string :kind, null: false
7
7
  t.string :action, null: false
8
8
  t.string :author_id
@@ -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 Testimonials
7
8
  module Generators
@@ -15,6 +16,7 @@ module Testimonials
15
16
  # bin/rails generate testimonials:tenant && bin/rails db:migrate
16
17
  class TenantGenerator < Rails::Generators::Base
17
18
  include ActiveRecord::Generators::Migration
19
+ include MigrationHelpers
18
20
 
19
21
  source_root File.expand_path('templates', __dir__)
20
22
 
@@ -29,12 +31,6 @@ module Testimonials
29
31
  say "\ntenant column queued. Run `rails db:migrate`, then set", :green
30
32
  say 'config.tenant in config/initializers/testimonials.rb to scope per tenant.'
31
33
  end
32
-
33
- private
34
-
35
- def migration_version
36
- "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
37
- end
38
34
  end
39
35
  end
40
36
  end
@@ -5,6 +5,10 @@ module Testimonials
5
5
  # works with zero configuration; the hooks below let an app decide who gets
6
6
  # prompted, who can triage, 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 = 'testimonials/application'
11
+
8
12
  # Shown in the widget ("Enjoying %{app}?") and interpolated into the
9
13
  # default questions. nil resolves to the Rails application name.
10
14
  attr_accessor :app_name
@@ -19,8 +23,26 @@ module Testimonials
19
23
 
20
24
  # Layout used by the built-in dashboard. Override this to render
21
25
  # Testimonials inside your app's admin shell, e.g. "admin/application".
26
+ # The lighter of the two ways to adopt a host's chrome; see
27
+ # base_controller_class for the other.
22
28
  attr_accessor :admin_layout
23
29
 
30
+ # The controller the DASHBOARD inherits from, as a String so it resolves
31
+ # lazily rather than at config time. Default: a plain
32
+ # 'ActionController::Base', where `authorize_admin` is the only gate.
33
+ #
34
+ # Name the controller your own admin already inherits from and the
35
+ # dashboard adopts that whole stack — layout, helpers, authentication, and
36
+ # any request context your before_actions set up (a `Current` attribute the
37
+ # layout reads, say). `admin_layout` covers only the layout, which leaves a
38
+ # host layout calling its own helpers to raise NameError under the engine's
39
+ # isolated namespace.
40
+ #
41
+ # Only the dashboard uses it. The widget's endpoints stay on the engine's
42
+ # own public controller, so an admin base controller here can never demand
43
+ # a staff session from a member leaving a review.
44
+ attr_accessor :base_controller_class
45
+
24
46
  # Resolve the current user for attribution (optional). Return an object
25
47
  # responding to #id, or nil. Receives the request.
26
48
  attr_accessor :current_user
@@ -117,7 +139,8 @@ module Testimonials
117
139
  @app_name = nil
118
140
  @enabled = ->(_request) { true }
119
141
  @authorize_admin = ->(_request) { Rails.env.development? }
120
- @admin_layout = 'testimonials/application'
142
+ @admin_layout = DEFAULT_ADMIN_LAYOUT
143
+ @base_controller_class = 'ActionController::Base'
121
144
  @current_user = ->(_request) {}
122
145
  @tenant = ->(_request) {}
123
146
  @user_display = lambda { |user|