keystone_ui-preferences 0.4.0 → 0.5.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: 497f7eeb505b63450d4e41595648c92f9acaba75494eb27c76ee901de7c60f9d
4
- data.tar.gz: 80e5ffc7ecfa86e0ef011b14cd939e4c2d39c9c4d3e8e086d6efd22756d8542d
3
+ metadata.gz: 132478f987e0dbbf12eeda5ac9c9e9fe14a9ca1ec8b8ce60618b29e272f5db8f
4
+ data.tar.gz: eefd3cd44bce3d9aa5f3dab0c3ff4f5ad4a882e15ab9fbd836964e46e631724a
5
5
  SHA512:
6
- metadata.gz: c79c4d859ae6f1aaa84cb608c626e69a13fb0aa908c8f373a6b11f07a49c8302d39bbcce5ac02228f94b5dd521a89225f000fd3c88277ef48b5e93053d840394
7
- data.tar.gz: f89d730b80476e7c455cc8eba394adab7b3a9c39199395143236cad08dca89e9331032b8fea268be0ee2f506643b3cf9fbbc105a7fac1b24ddc27e150f1b19d8
6
+ metadata.gz: da15030c2de6a478df7106c48218dfe8bcd2197f592f68ea8147b5b6fa389b5c65b4e30699a1480df86f13ec1d8069a2da2058c38550e8ed539e74ccab8dfe74
7
+ data.tar.gz: be3c817d9c47d16df821adeb5cf7fbe2e6cb884ad48e3711e00f2d270619c25368a202d0364593fa15bea9f7eefc2651027a3f80fa225fd4fe4645b6c692d732
data/README.md CHANGED
@@ -18,6 +18,13 @@ bin/rails generate keystone_ui:preferences:install
18
18
  bin/rails db:migrate
19
19
  ```
20
20
 
21
+ After updating the gem, run its update generator to copy any migrations added since it was installed:
22
+
23
+ ```bash
24
+ bin/rails generate keystone_ui:preferences:update
25
+ bin/rails db:migrate
26
+ ```
27
+
21
28
  Mount the engine in `config/routes.rb`. A table's Columns menu saves to it:
22
29
 
23
30
  ```ruby
@@ -94,6 +101,36 @@ end
94
101
 
95
102
  Each key has the members-choose switch and, when the admin has a saved layout of their own, a button that makes it the account's layout.
96
103
 
104
+ ## Navigation placement
105
+
106
+ A person chooses where keystone_ui draws the desktop navigation, at the top or as a sidebar on the left or the right, in a settings_hub section the app registers:
107
+
108
+ ```ruby
109
+ SettingsHub.section :navigation_placement, area: :user, title: "Navigation",
110
+ renders: "keystone_ui/preferences/settings/navigation_placement",
111
+ runs: "KeystoneUi::Preferences::PickNavigationPlacement"
112
+ ```
113
+
114
+ The section offers Top, Left and Right, with the placement that applies to the person selected, or Top when nothing is saved. Saving keeps `"placement": "left"` for that person under the `navigation` key, leaving any saved tab order as it was, and keystone_ui's navigation reads it on the next page they open. A placement other than `top`, `left` or `right` is refused with a message, and nothing is saved.
115
+
116
+ The `navigation` key follows the account layouts above, so an account that turns off members-choose for it draws its own placement and tab order for every member.
117
+
118
+ While it is off, the placement and tab order sections say the account sets the navigation for everyone and offer no save. A placement, tab order or reset sent anyway is refused with a message, and nothing is saved.
119
+
120
+ ## Tab order
121
+
122
+ A person sets the order of the navigation's groups, and of the tabs in each group, in a settings_hub section the app registers:
123
+
124
+ ```ruby
125
+ SettingsHub.section :navigation_order, area: :user, title: "Tab order",
126
+ renders: "keystone_ui/preferences/settings/navigation_order",
127
+ runs: "KeystoneUi::Preferences::PickNavigationOrder"
128
+ ```
129
+
130
+ The section lists the groups declared with keystone_ui's `navigation_group`, each with the tabs the person may see, in the order that applies to them. A group with no tab they may see is left out. Each group and tab has an Up and a Down button, and pressing one saves the order with it moved one place, so the page works in a Hotwire Native web view with no script and nothing to drag. At phone width each row's buttons sit under its name.
131
+
132
+ Saving keeps `"order": [{ "group": "Admin" }, { "group": "Sales", "tabs": ["orders", "quotes"] }]` for that person under the `navigation` key, naming groups by label and tabs by key, and leaves their saved placement as it was. keystone_ui draws the navigation in that order on the next page they open, with anything the order does not name after it in its declared order. Reset removes the saved order, so the navigation goes back to the declared order. An order that is not a list of groups is refused with a message, and nothing is saved.
133
+
97
134
  ## What is stored
98
135
 
99
136
  One row per owner and component key, in `keystone_ui_preferences_component_preferences`:
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KeystoneUi
4
+ module Preferences
5
+ class NavigationArrangement
6
+ def initialize(arranged)
7
+ @arranged = arranged
8
+ end
9
+
10
+ def group_moved(index, step)
11
+ moved(order, index, step)
12
+ end
13
+
14
+ def tab_moved(group_index, tab_index, step)
15
+ order.tap { |groups| moved(groups[group_index]["tabs"], tab_index, step) }
16
+ end
17
+
18
+ def order
19
+ @arranged.map { |group, tabs| { "group" => group.label, "tabs" => tabs.map { |tab| tab.key.to_s } } }
20
+ end
21
+
22
+ private
23
+
24
+ def moved(items, index, step)
25
+ items.insert(index + step, items.delete_at(index))
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module KeystoneUi
6
+ module Preferences
7
+ class PickNavigationOrder
8
+ def initialize(values:, person: nil, account: nil)
9
+ @person = person
10
+ @account = account
11
+ @values = values
12
+ end
13
+
14
+ def call
15
+ return Refusal.new("Your account sets the navigation for everyone, so this was not saved.") unless members_choose?
16
+ return reset if @values[:reset] == "1"
17
+ return Refusal.new("That navigation order is too large to save.") if @values[:order].to_s.bytesize > KeystoneUi::Preferences.configuration.max_value_bytes
18
+
19
+ order = submitted_order
20
+ return Refusal.new("Choose an order for the navigation's groups and tabs.") unless order.is_a?(Array)
21
+
22
+ preference.update!(value: preference.value.to_h.merge("order" => order))
23
+ Kept.new
24
+ end
25
+
26
+ private
27
+
28
+ def members_choose?
29
+ LayoutChoice.new(person: @person, account: @account, component_key: "navigation").members_choose?
30
+ end
31
+
32
+ def reset
33
+ preference.update!(value: preference.value.to_h.except("order"))
34
+ Kept.new
35
+ end
36
+
37
+ def preference
38
+ @preference ||= ComponentPreference.find_or_initialize_by(owner: @person, component_key: "navigation")
39
+ end
40
+
41
+ def submitted_order
42
+ JSON.parse(@values[:order].to_s)
43
+ rescue JSON::ParserError
44
+ nil
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KeystoneUi
4
+ module Preferences
5
+ class PickNavigationPlacement
6
+ PLACEMENTS = %w[top left right].freeze
7
+
8
+ def initialize(values:, person: nil, account: nil)
9
+ @person = person
10
+ @account = account
11
+ @values = values
12
+ end
13
+
14
+ def call
15
+ return Refusal.new("Your account sets the navigation for everyone, so this was not saved.") unless members_choose?
16
+ return Refusal.new("Choose Top, Left or Right for the navigation.") unless PLACEMENTS.include?(@values[:placement])
17
+
18
+ preference = ComponentPreference.find_or_initialize_by(owner: @person, component_key: "navigation")
19
+ preference.update!(value: preference.value.to_h.merge("placement" => @values[:placement]))
20
+ Kept.new
21
+ end
22
+
23
+ private
24
+
25
+ def members_choose?
26
+ LayoutChoice.new(person: @person, account: @account, component_key: "navigation").members_choose?
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,58 @@
1
+ <% choice = KeystoneUi::Preferences::LayoutChoice.new(person: person, account: account, component_key: "navigation") %>
2
+ <% saved_order = choice.value&.dig("order") || [] %>
3
+
4
+ <% if choice.members_choose? %>
5
+ <% permitted = KeystoneUi.configuration.navigation_groups.map { |group| [ group, group.tabs_permitted_for(self) ] }.reject { |_group, tabs| tabs.empty? } %>
6
+ <% arranged = KeystoneUi::NavigationOrder.new(saved_order).arrange(permitted) %>
7
+ <% arrangement = KeystoneUi::Preferences::NavigationArrangement.new(arranged) %>
8
+
9
+ <% arranged.each_with_index do |(group, tabs), group_index| %>
10
+ <div data-navigation-group="<%= group.label %>">
11
+ <%= ui_grid(cols: { default: 1, sm: 2 }, gap: :sm) do %>
12
+ <p data-group-label><strong><%= group.label %></strong></p>
13
+ <%= ui_grid(cols: { default: 2 }, gap: :sm) do %>
14
+ <% if group_index.positive? %>
15
+ <%= form_with(url: submit_url, method: :patch, data: { turbo: false, group_move: "up" }) do %>
16
+ <%= hidden_field_tag "order", arrangement.group_moved(group_index, -1).to_json, id: nil %>
17
+ <%= ui_button(label: "Up", type: :submit, variant: :secondary, size: :sm) %>
18
+ <% end %>
19
+ <% end %>
20
+ <% if group_index < arranged.size - 1 %>
21
+ <%= form_with(url: submit_url, method: :patch, data: { turbo: false, group_move: "down" }) do %>
22
+ <%= hidden_field_tag "order", arrangement.group_moved(group_index, 1).to_json, id: nil %>
23
+ <%= ui_button(label: "Down", type: :submit, variant: :secondary, size: :sm) %>
24
+ <% end %>
25
+ <% end %>
26
+ <% end %>
27
+ <% end %>
28
+ <% tabs.each_with_index do |tab, tab_index| %>
29
+ <div data-navigation-tab="<%= tab.key %>">
30
+ <%= ui_grid(cols: { default: 1, sm: 2 }, gap: :sm) do %>
31
+ <span data-tab-label><%= tab.label %></span>
32
+ <%= ui_grid(cols: { default: 2 }, gap: :sm) do %>
33
+ <% if tab_index.positive? %>
34
+ <%= form_with(url: submit_url, method: :patch, data: { turbo: false, tab_move: "up" }) do %>
35
+ <%= hidden_field_tag "order", arrangement.tab_moved(group_index, tab_index, -1).to_json, id: nil %>
36
+ <%= ui_button(label: "Up", type: :submit, variant: :secondary, size: :sm) %>
37
+ <% end %>
38
+ <% end %>
39
+ <% if tab_index < tabs.size - 1 %>
40
+ <%= form_with(url: submit_url, method: :patch, data: { turbo: false, tab_move: "down" }) do %>
41
+ <%= hidden_field_tag "order", arrangement.tab_moved(group_index, tab_index, 1).to_json, id: nil %>
42
+ <%= ui_button(label: "Down", type: :submit, variant: :secondary, size: :sm) %>
43
+ <% end %>
44
+ <% end %>
45
+ <% end %>
46
+ <% end %>
47
+ </div>
48
+ <% end %>
49
+ </div>
50
+ <% end %>
51
+
52
+ <%= form_with(url: submit_url, method: :patch, data: { turbo: false }) do %>
53
+ <%= hidden_field_tag "reset", "1", id: nil %>
54
+ <%= ui_button(label: "Reset", type: :submit, variant: :secondary) %>
55
+ <% end %>
56
+ <% else %>
57
+ <%= ui_alert(message: "Your account sets the navigation for everyone.") %>
58
+ <% end %>
@@ -0,0 +1,15 @@
1
+ <% choice = KeystoneUi::Preferences::LayoutChoice.new(person: person, account: account, component_key: "navigation") %>
2
+ <% current = choice.value&.dig("placement") || "top" %>
3
+
4
+ <% if choice.members_choose? %>
5
+ <%= form_with(url: submit_url, method: :patch, data: { turbo: false }) do %>
6
+ <%= ui_grid(cols: { default: 1, sm: 3 }, gap: :sm) do %>
7
+ <% { "top" => "Top", "left" => "Left", "right" => "Right" }.each do |placement, label| %>
8
+ <%= ui_radio_card(name: "placement", value: placement, label: label, checked: placement == current) %>
9
+ <% end %>
10
+ <% end %>
11
+ <%= ui_button(label: "Save", type: :submit) %>
12
+ <% end %>
13
+ <% else %>
14
+ <%= ui_alert(message: "Your account sets the navigation for everyone.") %>
15
+ <% end %>
@@ -1,7 +1,7 @@
1
1
  class CreateKeystoneUiPreferencesComponentPreferences < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
2
  def change
3
3
  create_table :keystone_ui_preferences_component_preferences do |t|
4
- t.references :owner, polymorphic: true, null: false
4
+ t.references :owner, polymorphic: true, null: false, index: false
5
5
  t.string :component_key, null: false
6
6
  t.json :value, null: false, default: {}
7
7
  t.boolean :members_choose, default: true, null: false
@@ -0,0 +1,5 @@
1
+ class RemoveExtraOwnerIndexFromKeystoneUiPreferencesComponentPreferences < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
+ def change
3
+ remove_index :keystone_ui_preferences_component_preferences, %i[owner_type owner_id], name: "index_keystone_ui_preferences_component_preferences_on_owner", if_exists: true
4
+ end
5
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/active_record"
5
+
6
+ module KeystoneUi
7
+ module Preferences
8
+ module Generators
9
+ class UpdateGenerator < Rails::Generators::Base
10
+ include ActiveRecord::Generators::Migration
11
+
12
+ source_root File.expand_path("templates", __dir__)
13
+
14
+ desc "Updates KeystoneUi::Preferences: copies migrations added since it was installed."
15
+
16
+ def copy_remove_extra_owner_index_migration
17
+ migration_template(
18
+ "remove_extra_owner_index_from_keystone_ui_preferences_component_preferences.rb.erb",
19
+ "db/migrate/remove_extra_owner_index_from_keystone_ui_preferences_component_preferences.rb"
20
+ )
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module KeystoneUi
4
4
  module Preferences
5
- VERSION = "0.4.0"
5
+ VERSION = "0.5.0"
6
6
  end
7
7
  end
@@ -1,4 +1,5 @@
1
1
  require "keystone_ui"
2
+ require "keystone_ui/navigation_order"
2
3
  require "keystone_ui/preferences/version"
3
4
  require "keystone_ui/preferences/configuration"
4
5
  require "keystone_ui/preferences/engine"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: keystone_ui-preferences
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tyler Schneider
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: 0.33.1
18
+ version: 0.38.0
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
- version: 0.33.1
25
+ version: 0.38.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: railties
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -83,12 +83,19 @@ files:
83
83
  - app/models/keystone_ui/preferences/component_preference.rb
84
84
  - app/models/keystone_ui/preferences/kept.rb
85
85
  - app/models/keystone_ui/preferences/layout_choice.rb
86
+ - app/models/keystone_ui/preferences/navigation_arrangement.rb
86
87
  - app/models/keystone_ui/preferences/pick_account_layouts.rb
88
+ - app/models/keystone_ui/preferences/pick_navigation_order.rb
89
+ - app/models/keystone_ui/preferences/pick_navigation_placement.rb
87
90
  - app/models/keystone_ui/preferences/refusal.rb
88
91
  - app/views/keystone_ui/preferences/settings/_account_layouts.html.erb
92
+ - app/views/keystone_ui/preferences/settings/_navigation_order.html.erb
93
+ - app/views/keystone_ui/preferences/settings/_navigation_placement.html.erb
89
94
  - config/routes.rb
90
95
  - lib/generators/keystone_ui/preferences/install/install_generator.rb
91
96
  - lib/generators/keystone_ui/preferences/install/templates/create_keystone_ui_preferences_component_preferences.rb.erb
97
+ - lib/generators/keystone_ui/preferences/update/templates/remove_extra_owner_index_from_keystone_ui_preferences_component_preferences.rb.erb
98
+ - lib/generators/keystone_ui/preferences/update/update_generator.rb
92
99
  - lib/keystone_ui-preferences.rb
93
100
  - lib/keystone_ui/preferences.rb
94
101
  - lib/keystone_ui/preferences/configuration.rb