demo_mode 4.0.0 → 4.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 77c2ff747d26dd0fd6ac69492e7a66c4af1cb23c5b85f0a8eefa9a300e0eb732
4
- data.tar.gz: c3f90ddf15771026c987695e955f4a5e392c3993271cb6dcaab7d75c6343ab94
3
+ metadata.gz: 76f5606300169245566aec753daa16508d60cbaee8509a096234aa1a2ffd8cce
4
+ data.tar.gz: adffd7d7b4e688cf1c15ac1113d64f354a35dce5aa5754b694b7df3246636f32
5
5
  SHA512:
6
- metadata.gz: f3b58322ad1b904486a7c00bd3c634c7d06083b1a004dc7de77c8e2f83642b354a595ca1a57d13740cf2bf3b911c5f946d758e425730c5f51b6a0ebabf5cb829
7
- data.tar.gz: e74f95b5e19180a2aed8129ac7ee1f22507db201d2707403f7039161e06e5b597427d7e951dd9936cbdbf64e2d1a0afec01db5a722481eac02bf5df3070666e7
6
+ metadata.gz: cc599fe84994691f54ecfe49af4e1b7f6e92561be78c26e0a8cf9e240ff1944e9d3c0a2825f75bd1a7b0e08ee5ac3cfac86009432c342354bfcf801b4dfa6272
7
+ data.tar.gz: b866a7902a4068fbc65400bda015908e8eae0a7c12141499cd24eedbbba8800b0de978da58c8f2fae0f1b9b6ba839dd28818d0e4f87291837e5584a317b8375a
data/README.md CHANGED
@@ -39,6 +39,7 @@ To learn more about how we use `demo_mode` at **Betterment**, check out :sparkle
39
39
  - [Installation](#installation)
40
40
  - [App-Specific Setup](#app-specific-setup)
41
41
  - [Defining Personas](#defining-personas)
42
+ - [Grouping Personas](#grouping-personas)
42
43
  - [Customizing the Design](#customizing-the-design)
43
44
  - [Optional Features](#optional-features)
44
45
  - [The "Sign Up" Link](#the-sign-up-link)
@@ -273,6 +274,46 @@ end
273
274
  If defined, the non-variant `sign_in_as` will show up as "default" in the
274
275
  dropdown.
275
276
 
277
+ ### Grouping Personas
278
+
279
+ When an app has many personas, the single table can become hard to scan. To
280
+ improve the end-user experience, you can organize your persona files into subfolders
281
+ (e.g. `config/personas/orders...`, `config/personas/advanced/...`) and then enable
282
+ the following setting to collapse those personas into labeled groupings:
283
+
284
+ ```ruby
285
+ DemoMode.configure do
286
+ group_by_folder true
287
+ end
288
+ ```
289
+
290
+ Alternatively, you can set a group name directly on any persona:
291
+
292
+ ```ruby
293
+ DemoMode.add_persona do
294
+ group 'Playwright testing'
295
+ # ...
296
+ end
297
+ ```
298
+
299
+ To directly control page ordering, use the `groups` config:
300
+
301
+ ```ruby
302
+ DemoMode.configure do
303
+ # Specify group order directly:
304
+ groups %w(transactions activity locked)
305
+
306
+ # ...or specify friendly labels along with the order:
307
+ groups(
308
+ 'orders' => 'Orders',
309
+ 'orders/checkout' => 'Checkout',
310
+ )
311
+
312
+ # If you prefer, move ungrouped personas to the bottom:
313
+ groups %w(inbound outbound), ungrouped_first: false
314
+ end
315
+ ```
316
+
276
317
  ## Customizing the Design
277
318
 
278
319
  To supply your own branding, you can override the logo
@@ -0,0 +1,8 @@
1
+ <% named_groups.each do |name, personas| %>
2
+ <section>
3
+ <details class="persona-group">
4
+ <summary><%= name %> (<%= personas.count %>)</summary>
5
+ <%= render 'persona_table', personas: personas %>
6
+ </details>
7
+ </section>
8
+ <% end %>
@@ -0,0 +1,28 @@
1
+ <table class="persona-table">
2
+ <thead>
3
+ <tr>
4
+ <th></th>
5
+ <th>Persona name</th>
6
+ <th>Variant</th>
7
+ <th>Features</th>
8
+ </tr>
9
+ </thead>
10
+ <tbody>
11
+ <% personas.each do |persona| %>
12
+ <tr class="<%= persona.css_class %>">
13
+ <td>
14
+ <%= form_for @session, html: { id: persona.name.to_s.underscore } do |f| %>
15
+ <%= f.hidden_field :persona_name, value: persona.name %>
16
+ <%= f.submit 'Sign In', class: 'secondary small' %>
17
+ <% @f = f %>
18
+ <% end %>
19
+ </td>
20
+ <td><%= persona.name.to_s.titleize %></td>
21
+ <td>
22
+ <%= render 'variant_dropdown', f: @f, persona: persona %>
23
+ </td>
24
+ <td><%= persona.features.join(", ") %></td>
25
+ </tr>
26
+ <% end %>
27
+ </tbody>
28
+ </table>
@@ -0,0 +1,5 @@
1
+ <% if personas.any? %>
2
+ <section>
3
+ <%= render 'persona_table', personas: personas %>
4
+ </section>
5
+ <% end %>
@@ -23,35 +23,12 @@
23
23
  <section>
24
24
  <input type="text" placeholder="Search..." data-table="persona-table" data-behavior="table-filter" />
25
25
  </section>
26
- <section>
27
- <table class="persona-table">
28
- <thead>
29
- <tr>
30
- <th></th>
31
- <th>Persona name</th>
32
- <th>Variant</th>
33
- <th>Features</th>
34
- </tr>
35
- </thead>
36
- <tbody>
37
- <% DemoMode.standard_personas.each do |persona| %>
38
- <tr class="<%= persona.css_class %>">
39
- <td>
40
- <%= form_for @session, html: { id: persona.name.to_s.underscore } do |f| %>
41
- <%= f.hidden_field :persona_name, value: persona.name %>
42
- <%= f.submit 'Sign In', class: 'secondary small' %>
43
- <% @f = f %>
44
- <% end %>
45
- </td>
46
- <td><%= persona.name.to_s.titleize %></td>
47
- <td>
48
- <%= render 'variant_dropdown', f: @f, persona: persona %>
49
- </td>
50
- <td><%= persona.features.join(", ") %></td>
51
- </tr>
52
- <% end %>
53
- </tbody>
54
- </table>
55
- </section>
26
+ <% if DemoMode.ungrouped_first? %>
27
+ <%= render 'ungrouped_personas', personas: DemoMode.ungrouped_personas %>
28
+ <%= render 'grouped_personas', named_groups: DemoMode.personas_by_group %>
29
+ <% else %>
30
+ <%= render 'grouped_personas', named_groups: DemoMode.personas_by_group %>
31
+ <%= render 'ungrouped_personas', personas: DemoMode.ungrouped_personas %>
32
+ <% end %>
56
33
  </article>
57
34
  <% end %>
@@ -55,7 +55,7 @@ class CleverSequence
55
55
  end
56
56
 
57
57
  def exists?(value)
58
- klass.public_send(finder_method, block.call(value))
58
+ klass.respond_to?(finder_method) && klass.public_send(finder_method, block.call(value))
59
59
  end
60
60
 
61
61
  # TODO: Move onto modern finder methods.
@@ -17,6 +17,9 @@ module DemoMode
17
17
  configurable_value(:log_level) { :debug }
18
18
  configurable_value(:minimum_pool_size) { 5 }
19
19
  configurable_boolean(:display_credentials)
20
+ configurable_boolean(:group_by_folder)
21
+ configurations << :groups
22
+ configurations << :ungrouped_first?
20
23
  configurations << :stylesheets
21
24
  configurations << :logo
22
25
  configurations << :loader
@@ -30,6 +33,15 @@ module DemoMode
30
33
  Rails.application.class.module_parent.name
31
34
  end
32
35
 
36
+ def groups(value = nil, **kwargs)
37
+ configure_groups(value, **kwargs) if value || kwargs.any?
38
+ @groups || []
39
+ end
40
+
41
+ def ungrouped_first?
42
+ @ungrouped_first.nil? ? true : @ungrouped_first
43
+ end
44
+
33
45
  def stylesheets
34
46
  @stylesheets ||= [
35
47
  "/demo_mode/assets/vendor/normalize-v8.0.1.css",
@@ -120,6 +132,11 @@ module DemoMode
120
132
 
121
133
  private
122
134
 
135
+ def configure_groups(value, **kwargs)
136
+ @ungrouped_first = kwargs.delete(:ungrouped_first)
137
+ @groups = kwargs.any? ? kwargs : value
138
+ end
139
+
123
140
  def auto_load_personas!
124
141
  Rails.root.glob("#{personas_path}/**/*.rb").sort.each do |persona_file|
125
142
  raise <<~ERROR if File.readlines(persona_file).grep(/DemoMode\.add_persona/).empty?
@@ -130,7 +147,10 @@ module DemoMode
130
147
  checksum = Digest::SHA256.hexdigest(File.read(persona_file))
131
148
  before_count = @personas.length
132
149
  load(persona_file)
133
- @personas[before_count..].each { |p| p.file_checksum = checksum }
150
+ @personas[before_count..].each do |p|
151
+ p.file_checksum = checksum
152
+ p.file_path = persona_file.to_s
153
+ end
134
154
  end
135
155
  end
136
156
  end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DemoMode
4
+ class Grouper
5
+ def initialize(personas, groups:)
6
+ @personas = personas
7
+ @groups = groups
8
+ end
9
+
10
+ def named
11
+ buckets.except(nil).sort_by { |group, _| sort_key(group) }
12
+ end
13
+
14
+ def named_groups
15
+ named.map { |group, personas| [name_for(group), personas] }
16
+ end
17
+
18
+ def ungrouped
19
+ buckets.fetch(nil, [])
20
+ end
21
+
22
+ def name_for(group)
23
+ return group unless groups.is_a?(Hash)
24
+
25
+ groups.stringify_keys[group.to_s] || group
26
+ end
27
+
28
+ private
29
+
30
+ attr_reader :personas, :groups
31
+
32
+ def buckets
33
+ personas.group_by(&:group)
34
+ end
35
+
36
+ def sort_key(group)
37
+ rank = group_keys.index(group.to_s)
38
+ [rank || Float::INFINITY, group.to_s]
39
+ end
40
+
41
+ def group_keys
42
+ (groups.is_a?(Hash) ? groups.keys : groups).map(&:to_s)
43
+ end
44
+ end
45
+ end
@@ -6,11 +6,19 @@ module DemoMode
6
6
  class Persona
7
7
  include ActiveModel::Model
8
8
 
9
- attr_accessor :name, :file_checksum
9
+ attr_accessor :name, :file_checksum, :file_path
10
10
 
11
11
  validates :name, presence: true
12
12
  validate :persona_must_have_at_least_one_feature
13
13
 
14
+ def group(name = nil)
15
+ if name
16
+ @group = name
17
+ else
18
+ @group || folder_group
19
+ end
20
+ end
21
+
14
22
  def icon(name_or_path = nil, &block)
15
23
  if block
16
24
  @icon = block
@@ -141,6 +149,15 @@ module DemoMode
141
149
 
142
150
  private
143
151
 
152
+ def folder_group
153
+ return unless DemoMode.group_by_folder?
154
+ return unless file_path
155
+
156
+ relative = Pathname.new(file_path).relative_path_from(Rails.root.join(DemoMode.personas_path))
157
+ folder = relative.dirname.to_s
158
+ folder unless folder == '.'
159
+ end
160
+
144
161
  def persona_must_have_at_least_one_feature
145
162
  errors.add(:base, <<~ERR) unless features.count >= 1
146
163
  Persona must have at least one feature.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DemoMode
4
- VERSION = '4.0.0'
4
+ VERSION = '4.1.0'
5
5
  end
data/lib/demo_mode.rb CHANGED
@@ -6,6 +6,7 @@ require 'demo_mode/version'
6
6
  require 'demo_mode/clever_sequence'
7
7
  require 'demo_mode/config'
8
8
  require 'demo_mode/engine'
9
+ require 'demo_mode/grouper'
9
10
  require 'demo_mode/persona'
10
11
 
11
12
  module DemoMode
@@ -48,6 +49,14 @@ module DemoMode
48
49
  personas - callout_personas
49
50
  end
50
51
 
52
+ def ungrouped_personas
53
+ grouper.ungrouped
54
+ end
55
+
56
+ def personas_by_group
57
+ grouper.named_groups
58
+ end
59
+
51
60
  def current_password
52
61
  Thread.current[:_demo_mode_password] ||= password.call
53
62
  end
@@ -70,6 +79,10 @@ module DemoMode
70
79
 
71
80
  private
72
81
 
82
+ def grouper
83
+ Grouper.new(standard_personas, groups: groups)
84
+ end
85
+
73
86
  def configuration
74
87
  @configuration ||= Config.new
75
88
  end
@@ -319,3 +319,13 @@ td {
319
319
  .hidden {
320
320
  display: none;
321
321
  }
322
+
323
+ .persona-group {
324
+ margin-top: 1rem;
325
+ }
326
+
327
+ .persona-group summary {
328
+ cursor: pointer;
329
+ font-weight: bold;
330
+ padding: 0.5rem 0;
331
+ }
@@ -55,14 +55,25 @@
55
55
  }
56
56
 
57
57
  function updateTable() {
58
- var table = document.querySelector(`.${input.dataset.table}`);
59
- if (table) {
58
+ var tables = document.querySelectorAll(`.${input.dataset.table}`);
59
+ if (tables.length === 0) {
60
+ alert("TableFilter cannot find its table");
61
+ return;
62
+ }
63
+ Array.prototype.forEach.call(tables, function (table) {
60
64
  Array.prototype.forEach.call(table.tBodies, function (tbody) {
61
65
  Array.prototype.forEach.call(tbody.rows, filter);
62
66
  });
63
- } else {
64
- alert("TableFilter cannot find its table");
65
- }
67
+ });
68
+ revealGroupsWithMatches();
69
+ }
70
+
71
+ function revealGroupsWithMatches() {
72
+ var groups = document.querySelectorAll("details.persona-group");
73
+ Array.prototype.forEach.call(groups, function (group) {
74
+ var hasMatch = !!group.querySelector("tbody tr[data-matches]");
75
+ group.open = inputValue !== "" && hasMatch;
76
+ });
66
77
  }
67
78
 
68
79
  function updateHistory() {
@@ -75,8 +86,13 @@
75
86
 
76
87
  function filter(row) {
77
88
  var text = row.textContent.toLowerCase().replace(/[^0-9a-zA-Z ]/g, "");
78
- row.style.display =
79
- text.indexOf(inputValue) === -1 ? "none" : "table-row";
89
+ var matches = text.indexOf(inputValue) !== -1;
90
+ row.style.display = matches ? "table-row" : "none";
91
+ if (matches) {
92
+ row.setAttribute("data-matches", "");
93
+ } else {
94
+ row.removeAttribute("data-matches");
95
+ }
80
96
  }
81
97
 
82
98
  function debounce(func, threshold) {
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: demo_mode
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.0
4
+ version: 4.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nathan Griffith
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2026-09-17 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: actionpack
@@ -343,6 +342,9 @@ files:
343
342
  - app/jobs/demo_mode/account_generation_job.rb
344
343
  - app/jobs/demo_mode/pool_hydration_job.rb
345
344
  - app/models/demo_mode/session.rb
345
+ - app/views/demo_mode/sessions/_grouped_personas.html.erb
346
+ - app/views/demo_mode/sessions/_persona_table.html.erb
347
+ - app/views/demo_mode/sessions/_ungrouped_personas.html.erb
346
348
  - app/views/demo_mode/sessions/_variant_dropdown.html.erb
347
349
  - app/views/demo_mode/sessions/new.html.erb
348
350
  - app/views/demo_mode/sessions/show.html.erb
@@ -366,6 +368,7 @@ files:
366
368
  - lib/demo_mode/config.rb
367
369
  - lib/demo_mode/engine.rb
368
370
  - lib/demo_mode/factory_bot_ext.rb
371
+ - lib/demo_mode/grouper.rb
369
372
  - lib/demo_mode/persona.rb
370
373
  - lib/demo_mode/tasks.rb
371
374
  - lib/demo_mode/version.rb
@@ -388,7 +391,6 @@ licenses:
388
391
  metadata:
389
392
  rubygems_mfa_required: 'true'
390
393
  changelog_uri: https://github.com/Betterment/demo_mode/blob/main/CHANGELOG.md
391
- post_install_message:
392
394
  rdoc_options: []
393
395
  require_paths:
394
396
  - lib
@@ -403,8 +405,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
403
405
  - !ruby/object:Gem::Version
404
406
  version: '0'
405
407
  requirements: []
406
- rubygems_version: 3.5.22
407
- signing_key:
408
+ rubygems_version: 4.0.21
408
409
  specification_version: 4
409
410
  summary: A configurable demo mode for your Rails app.
410
411
  test_files: []