keystone_ui 0.36.0 → 0.38.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/app/assets/javascripts/keystone_ui/column_picker_controller.js +19 -2
- data/app/components/keystone/ui/column_picker_component.html.erb +1 -0
- data/app/components/keystone/ui/column_picker_component.rb +5 -5
- data/app/components/keystone/ui/data_table_component.rb +6 -19
- data/app/components/keystone/ui/navigation_component.html.erb +48 -0
- data/app/components/keystone/ui/navigation_component.rb +72 -0
- data/app/components/keystone/ui/saved_layout.rb +46 -0
- data/app/helpers/keystone_ui_helper.rb +4 -0
- data/lib/keystone_ui/configuration.rb +15 -2
- data/lib/keystone_ui/navigation_group.rb +26 -0
- data/lib/keystone_ui/navigation_order.rb +35 -0
- data/lib/keystone_ui/safelist.rb +1 -0
- data/lib/keystone_ui/version.rb +1 -1
- data/the_local/agents/keystone_ui-develop.md +32 -3
- data/the_local/agents/keystone_ui-info.md +6 -2
- data/the_local/agents/keystone_ui-install.md +47 -2
- data/the_local/interface.yml +5 -0
- metadata +8 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ba03a37368cf5e1f7e0b18721a1eca9a04aae5dea7a5b3d8887e3db9956f9e8f
|
|
4
|
+
data.tar.gz: 6c0eb35d049ec308a68984f32cd787d8fa1c183057a975322844187f294c31fc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 36f95ca64410c3d39cae7355a0f1f7673a66e963ecf78a67d82a5559c74eef5ea02de3dc4668aef26fdab81b7b330d8d30dbb1a0aea658f13a188fdfdf296e27
|
|
7
|
+
data.tar.gz: 41c5aff5beccf97a8fac694a8cb6e4250e9b6c4456efb78dc1335df8c5a3be086bacae0b5a28292520666bfae44f7df5b8d6cc9c2fbe36f88f619c0a61e33cc1
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
2
|
|
|
3
3
|
export default class extends Controller {
|
|
4
|
-
static targets = ["menu", "option"]
|
|
4
|
+
static targets = ["menu", "option", "error"]
|
|
5
5
|
static values = { saveUrl: String }
|
|
6
6
|
|
|
7
7
|
connect() {
|
|
8
|
+
this.shown = this.optionTargets.map(option => [ option, this.checkboxIn(option).checked ])
|
|
8
9
|
this._close = this.close.bind(this)
|
|
9
10
|
document.addEventListener("click", this._close)
|
|
10
11
|
}
|
|
@@ -66,6 +67,10 @@ export default class extends Controller {
|
|
|
66
67
|
|
|
67
68
|
moved() {
|
|
68
69
|
this.changed = true
|
|
70
|
+
this.refreshMoveButtons()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
refreshMoveButtons() {
|
|
69
74
|
const options = this.optionTargets
|
|
70
75
|
options.forEach((option, index) => {
|
|
71
76
|
option.querySelector('[data-action="click->column-picker#moveUp"]').disabled = index === 0
|
|
@@ -99,8 +104,20 @@ export default class extends Controller {
|
|
|
99
104
|
"X-CSRF-Token": token
|
|
100
105
|
},
|
|
101
106
|
body: JSON.stringify({ hidden_columns: this.hiddenColumns(), column_order: this.columnOrder() })
|
|
102
|
-
}).then(() => {
|
|
107
|
+
}).then((response) => {
|
|
108
|
+
if (!response.ok) return this.failed()
|
|
109
|
+
|
|
103
110
|
Turbo.visit(window.location.href, { action: "replace" })
|
|
111
|
+
}).catch(() => this.failed())
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
failed() {
|
|
115
|
+
this.errorTarget.classList.remove("hidden")
|
|
116
|
+
this.shown.forEach(([ option, checked ]) => {
|
|
117
|
+
option.parentNode.insertBefore(option, null)
|
|
118
|
+
this.checkboxIn(option).checked = checked
|
|
119
|
+
option.querySelector("label").classList.toggle("ks-menu-option-hidden", !checked)
|
|
104
120
|
})
|
|
121
|
+
this.refreshMoveButtons()
|
|
105
122
|
}
|
|
106
123
|
}
|
|
@@ -11,6 +11,7 @@ module Keystone
|
|
|
11
11
|
OPTION_HIDDEN_CLASSES = "ks-menu-option-hidden"
|
|
12
12
|
OPTION_ROW_CLASSES = "flex items-center"
|
|
13
13
|
MOVE_BUTTON_CLASSES = "ks-menu-move text-sm"
|
|
14
|
+
ERROR_CLASSES = "ks-error hidden"
|
|
14
15
|
|
|
15
16
|
COLUMNS_ICON = <<~SVG.freeze
|
|
16
17
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-4 h-4">
|
|
@@ -20,18 +21,17 @@ module Keystone
|
|
|
20
21
|
|
|
21
22
|
attr_reader :save_url
|
|
22
23
|
|
|
23
|
-
def initialize(columns:, hidden_columns: [], save_url: nil)
|
|
24
|
-
@
|
|
25
|
-
@hidden_keys = Array(hidden_columns).map(&:to_sym).to_set
|
|
24
|
+
def initialize(columns:, hidden_columns: [], save_url: nil, layout: nil)
|
|
25
|
+
@layout = layout || SavedLayout.new(columns: columns, value: nil, default_hidden: hidden_columns)
|
|
26
26
|
@save_url = save_url
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
def hideable_columns
|
|
30
|
-
@columns.select(&:hideable?)
|
|
30
|
+
@layout.columns.select(&:hideable?)
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def hidden?(key)
|
|
34
|
-
@
|
|
34
|
+
@layout.hidden?(key)
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
def option_classes(key)
|
|
@@ -32,8 +32,9 @@ module Keystone
|
|
|
32
32
|
def initialize(items:, columns:, empty_message: nil, sort: nil, sort_direction: nil, sort_url: nil, hidden_columns: [], key: nil)
|
|
33
33
|
@items = items.to_a
|
|
34
34
|
@all_columns = columns.map { |col| normalize_column(col) }
|
|
35
|
-
@
|
|
36
|
-
@
|
|
35
|
+
@default_hidden = hidden_columns
|
|
36
|
+
@layout = SavedLayout.new(columns: @all_columns, value: nil, default_hidden: hidden_columns)
|
|
37
|
+
@columns = @layout.visible_columns
|
|
37
38
|
@key = key
|
|
38
39
|
@empty_message = empty_message
|
|
39
40
|
@sort = sort&.to_sym
|
|
@@ -63,7 +64,7 @@ module Keystone
|
|
|
63
64
|
def column_picker
|
|
64
65
|
return unless @save_url
|
|
65
66
|
|
|
66
|
-
@column_picker ||= ColumnPickerComponent.new(columns: @all_columns,
|
|
67
|
+
@column_picker ||= ColumnPickerComponent.new(columns: @all_columns, layout: @layout, save_url: @save_url)
|
|
67
68
|
end
|
|
68
69
|
|
|
69
70
|
def column_keys
|
|
@@ -151,25 +152,11 @@ module Keystone
|
|
|
151
152
|
saved = KeystoneUi.configuration.supplied_preference(helpers, @key)
|
|
152
153
|
return unless saved
|
|
153
154
|
|
|
154
|
-
@
|
|
155
|
-
@
|
|
156
|
-
@columns = visible_columns(@hidden_columns)
|
|
155
|
+
@layout = SavedLayout.new(columns: @all_columns, value: saved[:value], default_hidden: @default_hidden)
|
|
156
|
+
@columns = @layout.visible_columns
|
|
157
157
|
@save_url = saved[:save_url]
|
|
158
158
|
end
|
|
159
159
|
|
|
160
|
-
def ordered_columns(column_order)
|
|
161
|
-
return @all_columns unless column_order
|
|
162
|
-
|
|
163
|
-
positions = Array(column_order).map(&:to_sym).each_with_index.to_h
|
|
164
|
-
hideable = @all_columns.select(&:hideable?).sort_by.with_index { |col, index| [ positions.fetch(col.key, positions.size + index) ] }
|
|
165
|
-
@all_columns.map { |col| col.hideable? ? hideable.shift : col }
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
def visible_columns(hidden_columns)
|
|
169
|
-
hidden_keys = Array(hidden_columns).map(&:to_sym).to_set
|
|
170
|
-
@all_columns.reject { |col| col.hideable? && hidden_keys.include?(col.key) }
|
|
171
|
-
end
|
|
172
|
-
|
|
173
160
|
def visual_column_count
|
|
174
161
|
@columns.length + (actions? ? 1 : 0)
|
|
175
162
|
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<% if !anything_to_show? %>
|
|
2
|
+
<%= content %>
|
|
3
|
+
<% elsif sidebar? %>
|
|
4
|
+
<% sidebar = capture do %>
|
|
5
|
+
<nav class="<%= SIDEBAR_CLASSES %>">
|
|
6
|
+
<% if logo? %>
|
|
7
|
+
<div><%= logo %></div>
|
|
8
|
+
<% end %>
|
|
9
|
+
<% groups.each do |group, tabs| %>
|
|
10
|
+
<div class="<%= SIDEBAR_GROUP_CLASSES %>">
|
|
11
|
+
<p class="<%= sidebar_group_label_classes(tabs) %>"><%= group.label %></p>
|
|
12
|
+
<% tabs.each do |tab| %>
|
|
13
|
+
<a class="<%= sidebar_tab_classes(tab) %>" href="<%= tab.href_for(helpers) %>"><%= tab.label %></a>
|
|
14
|
+
<% end %>
|
|
15
|
+
</div>
|
|
16
|
+
<% end %>
|
|
17
|
+
<% if menus? %>
|
|
18
|
+
<div class="<%= SIDEBAR_MENUS_CLASSES %>"><%= menus %></div>
|
|
19
|
+
<% end %>
|
|
20
|
+
</nav>
|
|
21
|
+
<% end %>
|
|
22
|
+
<div class="<%= SIDEBAR_ROW_CLASSES %>">
|
|
23
|
+
<%= sidebar if sidebar_first? %>
|
|
24
|
+
<div class="<%= SIDEBAR_CONTENT_CLASSES %>"><%= content %></div>
|
|
25
|
+
<%= sidebar unless sidebar_first? %>
|
|
26
|
+
</div>
|
|
27
|
+
<% else %>
|
|
28
|
+
<div class="<%= TOP_BAR_CLASSES %>">
|
|
29
|
+
<%= render Keystone::Ui::NavbarComponent.new(sticky: false) do |navbar| %>
|
|
30
|
+
<% if logo? %>
|
|
31
|
+
<% navbar.with_logo do %><%= logo %><% end %>
|
|
32
|
+
<% end %>
|
|
33
|
+
<% navbar.with_desktop_links do %>
|
|
34
|
+
<% groups.each do |group, tabs| %>
|
|
35
|
+
<%= render Keystone::Ui::NavDropdownComponent.new(title: group.label, area: group.label, active: holds_current?(tabs)) do %>
|
|
36
|
+
<% tabs.each do |tab| %>
|
|
37
|
+
<%= render Keystone::Ui::NavItemComponent.new(label: tab.label, href: tab.href_for(helpers), active: current?(tab)) %>
|
|
38
|
+
<% end %>
|
|
39
|
+
<% end %>
|
|
40
|
+
<% end %>
|
|
41
|
+
<% end %>
|
|
42
|
+
<% if menus? %>
|
|
43
|
+
<% navbar.with_desktop_right do %><%= menus %><% end %>
|
|
44
|
+
<% end %>
|
|
45
|
+
<% end %>
|
|
46
|
+
</div>
|
|
47
|
+
<%= content %>
|
|
48
|
+
<% end %>
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "keystone_ui/navigation_order"
|
|
4
|
+
|
|
5
|
+
module Keystone
|
|
6
|
+
module Ui
|
|
7
|
+
class NavigationComponent < ViewComponent::Base
|
|
8
|
+
TOP_BAR_CLASSES = "hidden lg:block sticky top-0 z-40"
|
|
9
|
+
SIDEBAR_ROW_CLASSES = "lg:flex"
|
|
10
|
+
SIDEBAR_CLASSES = "ks-sidebar hidden lg:flex lg:sticky lg:top-0 lg:h-screen shrink-0 overflow-y-auto"
|
|
11
|
+
SIDEBAR_GROUP_CLASSES = "flex flex-col"
|
|
12
|
+
SIDEBAR_GROUP_LABEL_CLASSES = "ks-sidebar-group-label"
|
|
13
|
+
SIDEBAR_TAB_CLASSES = "ks-sidebar-tab"
|
|
14
|
+
ACTIVE_CLASS = "active"
|
|
15
|
+
SIDEBAR_MENUS_CLASSES = "mt-auto"
|
|
16
|
+
SIDEBAR_CONTENT_CLASSES = "min-w-0 flex-1"
|
|
17
|
+
SIDEBAR_PLACEMENTS = %w[left right].freeze
|
|
18
|
+
|
|
19
|
+
renders_one :logo
|
|
20
|
+
renders_one :menus
|
|
21
|
+
|
|
22
|
+
attr_reader :groups
|
|
23
|
+
|
|
24
|
+
def before_render
|
|
25
|
+
permitted = KeystoneUi.configuration.navigation_groups
|
|
26
|
+
.map { |group| [ group, group.tabs_permitted_for(helpers) ] }
|
|
27
|
+
.reject { |_group, tabs| tabs.empty? }
|
|
28
|
+
@groups = KeystoneUi::NavigationOrder.new(saved_navigation["order"] || []).arrange(permitted)
|
|
29
|
+
@current_tab = KeystoneUi.configuration.supplied_current_tab(helpers)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def current?(tab)
|
|
33
|
+
tab.key == @current_tab
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def holds_current?(tabs)
|
|
37
|
+
tabs.any? { |tab| current?(tab) }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def sidebar_group_label_classes(tabs)
|
|
41
|
+
[ SIDEBAR_GROUP_LABEL_CLASSES, (ACTIVE_CLASS if holds_current?(tabs)) ].compact.join(" ")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def sidebar_tab_classes(tab)
|
|
45
|
+
[ SIDEBAR_TAB_CLASSES, (ACTIVE_CLASS if current?(tab)) ].compact.join(" ")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def anything_to_show?
|
|
49
|
+
groups.any? || logo? || menus?
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def sidebar?
|
|
53
|
+
SIDEBAR_PLACEMENTS.include?(placement)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def sidebar_first?
|
|
57
|
+
placement == "left"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def placement
|
|
63
|
+
saved_navigation["placement"]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def saved_navigation
|
|
67
|
+
value = KeystoneUi.configuration.supplied_preference(helpers, :navigation)&.dig(:value)
|
|
68
|
+
value.is_a?(Hash) ? value : {}
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Keystone
|
|
4
|
+
module Ui
|
|
5
|
+
class SavedLayout
|
|
6
|
+
def initialize(columns:, value:, default_hidden:)
|
|
7
|
+
@columns = columns
|
|
8
|
+
@value = value.is_a?(Hash) ? value : {}
|
|
9
|
+
@default_hidden = default_hidden
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def columns
|
|
13
|
+
@ordered_columns ||= ordered_columns
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def visible_columns
|
|
17
|
+
columns.reject { |column| hidden?(column.key) }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def hidden?(key)
|
|
21
|
+
column = @columns.find { |col| col.key == key.to_sym }
|
|
22
|
+
column&.hideable? && hidden_keys.include?(key.to_sym)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def ordered_columns
|
|
28
|
+
column_order = saved_list("column_order")
|
|
29
|
+
return @columns unless column_order
|
|
30
|
+
|
|
31
|
+
positions = Array(column_order).map(&:to_sym).each_with_index.to_h
|
|
32
|
+
hideable = @columns.select(&:hideable?).sort_by.with_index { |col, index| positions.fetch(col.key, positions.size + index) }
|
|
33
|
+
@columns.map { |col| col.hideable? ? hideable.shift : col }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def hidden_keys
|
|
37
|
+
@hidden_keys ||= Array(saved_list("hidden_columns") || @default_hidden).map(&:to_sym).to_set
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def saved_list(name)
|
|
41
|
+
list = @value[name]
|
|
42
|
+
list if list.is_a?(Array)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -141,6 +141,10 @@ module KeystoneUiHelper
|
|
|
141
141
|
render Keystone::Ui::NavbarComponent.new(**args), &block
|
|
142
142
|
end
|
|
143
143
|
|
|
144
|
+
def ui_navigation(&block)
|
|
145
|
+
render Keystone::Ui::NavigationComponent.new, &block
|
|
146
|
+
end
|
|
147
|
+
|
|
144
148
|
def ui_nav_item(**args)
|
|
145
149
|
render Keystone::Ui::NavItemComponent.new(**args)
|
|
146
150
|
end
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "keystone_ui/navigation_group"
|
|
4
|
+
|
|
3
5
|
module KeystoneUi
|
|
4
6
|
class Configuration
|
|
5
|
-
attr_accessor :accent, :surface, :theme_mode_supplier, :look_supplier, :trail_supplier, :preference_supplier
|
|
6
|
-
attr_reader :tailwind_imports, :tailwind_sources, :looks, :default_look
|
|
7
|
+
attr_accessor :accent, :surface, :theme_mode_supplier, :look_supplier, :trail_supplier, :preference_supplier, :current_tab_supplier
|
|
8
|
+
attr_reader :tailwind_imports, :tailwind_sources, :looks, :default_look, :navigation_groups
|
|
7
9
|
|
|
8
10
|
def initialize
|
|
9
11
|
@accent = :blue
|
|
@@ -11,6 +13,7 @@ module KeystoneUi
|
|
|
11
13
|
@tailwind_imports = []
|
|
12
14
|
@tailwind_sources = []
|
|
13
15
|
@looks = {}
|
|
16
|
+
@navigation_groups = []
|
|
14
17
|
end
|
|
15
18
|
|
|
16
19
|
def default_look=(name)
|
|
@@ -22,6 +25,12 @@ module KeystoneUi
|
|
|
22
25
|
@tailwind_imports << path.to_s
|
|
23
26
|
end
|
|
24
27
|
|
|
28
|
+
def navigation_group(label)
|
|
29
|
+
group = NavigationGroup.new(label)
|
|
30
|
+
yield(group)
|
|
31
|
+
@navigation_groups << group
|
|
32
|
+
end
|
|
33
|
+
|
|
25
34
|
def supplied_look(view)
|
|
26
35
|
look_supplier&.call(view)&.to_s
|
|
27
36
|
end
|
|
@@ -37,6 +46,10 @@ module KeystoneUi
|
|
|
37
46
|
def supplied_preference(view, key)
|
|
38
47
|
preference_supplier&.call(view, key)
|
|
39
48
|
end
|
|
49
|
+
|
|
50
|
+
def supplied_current_tab(view)
|
|
51
|
+
current_tab_supplier&.call(view)
|
|
52
|
+
end
|
|
40
53
|
end
|
|
41
54
|
|
|
42
55
|
def self.configuration
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module KeystoneUi
|
|
4
|
+
class NavigationGroup
|
|
5
|
+
Tab = Struct.new(:key, :label, :href, :permitted, keyword_init: true) do
|
|
6
|
+
def href_for(view)
|
|
7
|
+
href.respond_to?(:call) ? href.call(view) : href
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
attr_reader :label, :tabs
|
|
12
|
+
|
|
13
|
+
def initialize(label)
|
|
14
|
+
@label = label
|
|
15
|
+
@tabs = []
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def tabs_permitted_for(view)
|
|
19
|
+
tabs.select { |tab| tab.permitted.call(view) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def tab(key, label:, href:, permitted:)
|
|
23
|
+
@tabs << Tab.new(key: key, label: label, href: href, permitted: permitted)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module KeystoneUi
|
|
4
|
+
class NavigationOrder
|
|
5
|
+
def initialize(saved)
|
|
6
|
+
@saved = saved.is_a?(Array) ? saved.grep(Hash) : []
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def arrange(groups)
|
|
10
|
+
in_saved_order(groups, group_labels) { |group, _tabs| group.label }
|
|
11
|
+
.map { |group, tabs| [ group, arrange_tabs(group, tabs) ] }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def in_saved_order(items, saved_names)
|
|
17
|
+
items.each_with_index
|
|
18
|
+
.sort_by { |item, declared| [ saved_names.index(yield(item)) || saved_names.size, declared ] }
|
|
19
|
+
.map(&:first)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def group_labels
|
|
23
|
+
@saved.map { |entry| entry["group"] }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def arrange_tabs(group, tabs)
|
|
27
|
+
in_saved_order(tabs, saved_tab_keys(group)) { |tab| tab.key.to_s }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def saved_tab_keys(group)
|
|
31
|
+
tabs = @saved.find { |entry| entry["group"] == group.label }&.fetch("tabs", nil)
|
|
32
|
+
tabs.is_a?(Array) ? tabs : []
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
data/lib/keystone_ui/safelist.rb
CHANGED
|
@@ -37,6 +37,7 @@ module Keystone
|
|
|
37
37
|
Keystone::Ui::NavItemComponent,
|
|
38
38
|
Keystone::Ui::BottomNavItemComponent,
|
|
39
39
|
Keystone::Ui::NavbarComponent,
|
|
40
|
+
Keystone::Ui::NavigationComponent,
|
|
40
41
|
Keystone::Ui::BottomNavComponent,
|
|
41
42
|
Keystone::Ui::SettingsLinkComponent,
|
|
42
43
|
Keystone::Ui::FormPageComponent,
|
data/lib/keystone_ui/version.rb
CHANGED
|
@@ -112,6 +112,28 @@ outer element. See Conventions before using it.
|
|
|
112
112
|
`mobile_center`, `mobile_right`. Desktop slots are hidden below `lg:` and the
|
|
113
113
|
mobile slots above it. `desktop_right` renders only when `desktop_links` is
|
|
114
114
|
also filled.
|
|
115
|
+
- `ui_navigation` — no keywords, takes a block holding the page content. Placed
|
|
116
|
+
in the layout around `yield`, it draws the tabs the app declares with
|
|
117
|
+
`config.navigation_group` as a top bar of menus from `lg:` up, then the
|
|
118
|
+
content. It leaves out tabs whose permission check fails and groups with no
|
|
119
|
+
tab left, and below `lg:` it draws only the content. Inside the block, a
|
|
120
|
+
`navigation.with_logo do ... end` block puts the app's logo at the left end
|
|
121
|
+
of the top bar, and a `navigation.with_menus do ... end` block puts menus
|
|
122
|
+
such as the account menu and the user menu at its right end, so the layout
|
|
123
|
+
draws no second bar for them. When the preference supplied for
|
|
124
|
+
`:navigation` holds `{ "placement" => "left" }` or `"right"`, it draws a
|
|
125
|
+
sidebar on that side of the content from `lg:` up instead, with each group's
|
|
126
|
+
label above its tabs, the logo at its top and the menus at its bottom, and
|
|
127
|
+
below `lg:` the content takes the full width. Any other placement draws the
|
|
128
|
+
top bar. When that value holds an `"order"` list of
|
|
129
|
+
`{ "group" => label, "tabs" => [tab key strings] }` entries, both placements
|
|
130
|
+
draw the named groups and tabs first in that order, then the rest in
|
|
131
|
+
declared order, ignoring names no longer declared. When the app sets `config.current_tab_supplier`, the tab whose key
|
|
132
|
+
it returns for the page and that tab's group are shown as active in either
|
|
133
|
+
placement, and nothing is marked when it returns `nil`, a key no declared
|
|
134
|
+
tab has, or is not set. Check whether the app's Keystone UI
|
|
135
|
+
initializer declares navigation groups before adding desktop tabs by hand;
|
|
136
|
+
declaring them is `keystone_ui-install`'s job.
|
|
115
137
|
- `ui_nav_item(label:, href:, active: false)` — one desktop navigation link.
|
|
116
138
|
- `ui_nav_dropdown(title:, area:, active: false)` — takes a block. A navbar
|
|
117
139
|
dropdown; the block holds the menu links.
|
|
@@ -240,8 +262,11 @@ outer element. See Conventions before using it.
|
|
|
240
262
|
and the hideable ones fill the remaining places. With no `"column_order"` the
|
|
241
263
|
columns keep their declared order. Whenever the supplier
|
|
242
264
|
returns a save address, the table renders a "Columns" menu in a row above
|
|
243
|
-
itself, aligned right, that saves to it as `ui_column_picker` does
|
|
244
|
-
for a person with nothing saved
|
|
265
|
+
itself, aligned right, that saves to it as `ui_column_picker` does and shows
|
|
266
|
+
the same message when a save fails, including for a person with nothing saved
|
|
267
|
+
yet. That menu lists the hideable columns in
|
|
268
|
+
the order the table shows them and leaves ticked exactly the ones the table
|
|
269
|
+
shows. When the
|
|
245
270
|
supplier returns nothing, when no supplier is set, or when no `key:` is
|
|
246
271
|
passed, the table renders from `hidden_columns:` with no Columns menu. In
|
|
247
272
|
every case `hidden_columns:` is the table's default layout.
|
|
@@ -269,7 +294,11 @@ outer element. See Conventions before using it.
|
|
|
269
294
|
`X-CSRF-Token` header, then reloads the page. A menu closed with nothing
|
|
270
295
|
changed sends nothing. `column_order` lists every hideable column's key in
|
|
271
296
|
the menu's order when it closes. With no `save_url:` it sends nothing. The app must provide that endpoint and persist
|
|
272
|
-
both lists. The
|
|
297
|
+
both lists. The endpoint must answer with a success status when it has saved
|
|
298
|
+
them. When it answers with an error status, or the request cannot reach the
|
|
299
|
+
server, the menu does not reload the page: it shows "Your column changes were
|
|
300
|
+
not saved." under the Columns button, and puts its boxes and its order back
|
|
301
|
+
to what the table shows. The picker does not reorder the table: beside a table without
|
|
273
302
|
`key:`, the app must pass the table and the picker its columns in the saved
|
|
274
303
|
order itself. A table
|
|
275
304
|
given `key:` renders its own Columns menu when the supplier gives a save
|
|
@@ -150,11 +150,15 @@ holds the catalog.
|
|
|
150
150
|
table, at its right, and saves the user's choice there, even before anything
|
|
151
151
|
has been saved. The menu lists the hideable columns in the order the table
|
|
152
152
|
shows them, each with a box to show or hide it and buttons to move it up or
|
|
153
|
-
down.
|
|
153
|
+
down. The menu and the table read the same saved layout, so the menu's
|
|
154
|
+
ticked boxes and order always match the columns the table shows. An unticked column's name is greyed at once. Ticking boxes and moving
|
|
154
155
|
columns sends nothing while the menu is open. Closing the menu, with its
|
|
155
156
|
Columns button or by clicking anywhere outside it, saves the hidden columns
|
|
156
157
|
and the order together once, then reloads the page. Closing it with nothing
|
|
157
|
-
changed saves nothing.
|
|
158
|
+
changed saves nothing. When the save is refused or cannot reach the server,
|
|
159
|
+
the page does not reload. The Columns menu shows the message "Your
|
|
160
|
+
column changes were not saved." and puts its boxes and order back to what
|
|
161
|
+
the table shows.
|
|
158
162
|
A table with no key, or an app with no supplier, renders from its own call
|
|
159
163
|
alone. Setting up the supplier belongs to the install local.
|
|
160
164
|
- **Suggestions.** A form field can carry a list of suggested values. The
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: keystone_ui-install
|
|
3
|
-
description: Use to hook Keystone UI into a project — adding the gem, running the install generator to wire Tailwind, the Stimulus controllers and the layout's theme attributes, and configuring the palette, the theme mode supplier, registered looks, the breadcrumb trail supplier, the saved table layout supplier and extra Tailwind imports and sources.
|
|
3
|
+
description: Use to hook Keystone UI into a project — adding the gem, running the install generator to wire Tailwind, the Stimulus controllers and the layout's theme attributes, and configuring the palette, the theme mode supplier, registered looks, the breadcrumb trail supplier, the saved table layout supplier, the navigation groups and current tab supplier, and extra Tailwind imports and sources.
|
|
4
4
|
tools: Bash, Read, Edit
|
|
5
5
|
scope: UI — pages, forms, tables, navigation, dashboards
|
|
6
6
|
---
|
|
@@ -28,7 +28,12 @@ built on ViewComponent; hook it in before building any screen with those helpers
|
|
|
28
28
|
show pages that pass none, and their Back link when they pass none either),
|
|
29
29
|
`preference_supplier` (a callable that supplies the saved hidden columns and
|
|
30
30
|
column order of a data table given a `key:`, and the address its Columns menu
|
|
31
|
-
saves to),
|
|
31
|
+
saves to, and the navigation's placement under the key `:navigation`),
|
|
32
|
+
`navigation_group` (a labelled group of desktop tabs that `ui_navigation`
|
|
33
|
+
draws, each tab declared with `tab key, label:, href:, permitted:`),
|
|
34
|
+
`current_tab_supplier` (a callable that supplies the key of the declared tab
|
|
35
|
+
the current page belongs to, so `ui_navigation` marks it and its group
|
|
36
|
+
active),
|
|
32
37
|
and the `tailwind_imports` and `tailwind_sources` lists (extra CSS files and
|
|
33
38
|
scan paths added to the Tailwind build).
|
|
34
39
|
- `keystone_theme_attributes` — a layout helper placed inside the `<html>` tag.
|
|
@@ -183,6 +188,10 @@ built on ViewComponent; hook it in before building any screen with those helpers
|
|
|
183
188
|
config.look_supplier = ->(view) { view.current_user&.look }
|
|
184
189
|
config.trail_supplier = ->(view) { view.breadcrumb_trail }
|
|
185
190
|
config.preference_supplier = ->(view, key) { TablePreferences.for(view.current_user, key) }
|
|
191
|
+
config.navigation_group "Sales" do |group|
|
|
192
|
+
group.tab :quotes, label: "Quotes", href: ->(view) { view.quotes_path }, permitted: ->(view) { view.policy(Quote).index? }
|
|
193
|
+
end
|
|
194
|
+
config.current_tab_supplier = ->(view) { view.controller.try(:navigation_tab) }
|
|
186
195
|
config.tailwind_imports << "/absolute/path/to/extra.css"
|
|
187
196
|
config.tailwind_sources << "/absolute/path/to/components/**/*.{erb,rb}"
|
|
188
197
|
end
|
|
@@ -282,9 +291,45 @@ built on ViewComponent; hook it in before building any screen with those helpers
|
|
|
282
291
|
string keys, `"hidden_columns"` and `"column_order"`, or the saved order
|
|
283
292
|
is not applied. With no `save_url`, the saved layout applies and no menu
|
|
284
293
|
is shown.
|
|
294
|
+
- The action at `save_url` must answer a stored layout with a 2xx status.
|
|
295
|
+
Any other status, or a request that cannot reach the server, leaves the
|
|
296
|
+
page unreloaded. The table then shows "Your column changes were not
|
|
297
|
+
saved." beside its Columns button, and the menu's boxes and order go back
|
|
298
|
+
to what the table shows.
|
|
299
|
+
- For the key `:navigation` the supplier returns the person's navigation
|
|
300
|
+
placement as `{ value: { "placement" => "left" } }`, with `"left"`,
|
|
301
|
+
`"right"` or `"top"`. `ui_navigation` draws a sidebar on the left or the
|
|
302
|
+
right of the page content on desktop screens for the first two, and the
|
|
303
|
+
top bar for `"top"`, `nil`, a value with no `"placement"` string key, or
|
|
304
|
+
any other placement.
|
|
305
|
+
- The same `:navigation` value may hold the person's order of groups and
|
|
306
|
+
tabs under `"order"`, as
|
|
307
|
+
`[ { "group" => "Admin" }, { "group" => "Sales", "tabs" => [ "orders", "quotes" ] } ]`.
|
|
308
|
+
A group is named by its declared label and a tab by its key as a string.
|
|
309
|
+
`ui_navigation` draws the named groups and tabs first in that order, then
|
|
310
|
+
the rest in declared order, and ignores names no longer declared.
|
|
285
311
|
- A companion preferences gem may set this supplier for the app. Ask the
|
|
286
312
|
developer whether the app uses one, or which code stores each user's table
|
|
287
313
|
layouts, before writing the callable.
|
|
314
|
+
- `navigation_group "Label" do |group| ... end` — declares one menu of
|
|
315
|
+
desktop tabs, in the order the groups are declared. Inside the block,
|
|
316
|
+
`group.tab key, label:, href:, permitted:` adds a tab in order. `href:` is
|
|
317
|
+
an address or a callable that receives the view and returns one.
|
|
318
|
+
`permitted:` is a callable that receives the view and returns whether the
|
|
319
|
+
current person may see the tab. The layout renders them with
|
|
320
|
+
`ui_navigation` around `yield`, setting the app's logo with
|
|
321
|
+
`navigation.with_logo` and its account and user menus with
|
|
322
|
+
`navigation.with_menus` inside the block, so the layout draws no other
|
|
323
|
+
top bar. Ask the developer which tabs and groups the app has, and which
|
|
324
|
+
permission check guards each, before declaring them.
|
|
325
|
+
- `current_tab_supplier` — a callable that receives the view and returns the
|
|
326
|
+
key of the declared tab the current page belongs to, or `nil`.
|
|
327
|
+
`ui_navigation` shows that tab and its group as active in the top bar and
|
|
328
|
+
gives that tab and its group's label the `active` class in the sidebar.
|
|
329
|
+
`nil`, a key no declared tab has, or no supplier marks nothing. The
|
|
330
|
+
callable must answer for every page the layout draws, so a page with no
|
|
331
|
+
tab returns `nil` rather than raising. Ask the developer how a page names
|
|
332
|
+
its tab, such as a method on each controller, before writing the callable.
|
|
288
333
|
- `tailwind_imports` and `tailwind_sources` — lists to append to, never
|
|
289
334
|
assign. Each import becomes an `@import` line and each source becomes an
|
|
290
335
|
`@source` line in `keystone_source.css` on the next boot. They are for
|
data/the_local/interface.yml
CHANGED
|
@@ -39,6 +39,7 @@ develop:
|
|
|
39
39
|
- ui_action_menu
|
|
40
40
|
- ui_action_menu_item
|
|
41
41
|
- ui_navbar
|
|
42
|
+
- ui_navigation
|
|
42
43
|
- ui_nav_item
|
|
43
44
|
- ui_nav_dropdown
|
|
44
45
|
- ui_bottom_nav
|
|
@@ -71,6 +72,8 @@ sources:
|
|
|
71
72
|
- app/components/keystone/ui/figure_component.rb
|
|
72
73
|
- lib/generators/keystone/install_generator.rb
|
|
73
74
|
- lib/keystone_ui/configuration.rb
|
|
75
|
+
- lib/keystone_ui/navigation_group.rb
|
|
76
|
+
- lib/keystone_ui/navigation_order.rb
|
|
74
77
|
- lib/keystone_ui/missing_back_link.rb
|
|
75
78
|
- lib/keystone_ui/incomplete_trail.rb
|
|
76
79
|
- lib/keystone_ui/theme_choice.rb
|
|
@@ -131,6 +134,8 @@ sources:
|
|
|
131
134
|
- app/components/keystone/ui/nav_dropdown_component.rb
|
|
132
135
|
- app/components/keystone/ui/nav_item_component.rb
|
|
133
136
|
- app/components/keystone/ui/navbar_component.rb
|
|
137
|
+
- app/components/keystone/ui/navigation_component.rb
|
|
138
|
+
- app/components/keystone/ui/navigation_component.html.erb
|
|
134
139
|
- app/components/keystone/ui/option_card_component.rb
|
|
135
140
|
- app/components/keystone/ui/page_component.rb
|
|
136
141
|
- app/components/keystone/ui/page_component.html.erb
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: keystone_ui
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.38.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.
|
|
18
|
+
version: 0.14.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.
|
|
25
|
+
version: 0.14.0
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: view_component
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -161,6 +161,8 @@ files:
|
|
|
161
161
|
- app/components/keystone/ui/nav_item_component.rb
|
|
162
162
|
- app/components/keystone/ui/navbar_component.html.erb
|
|
163
163
|
- app/components/keystone/ui/navbar_component.rb
|
|
164
|
+
- app/components/keystone/ui/navigation_component.html.erb
|
|
165
|
+
- app/components/keystone/ui/navigation_component.rb
|
|
164
166
|
- app/components/keystone/ui/option_card_component.html.erb
|
|
165
167
|
- app/components/keystone/ui/option_card_component.rb
|
|
166
168
|
- app/components/keystone/ui/page_component.html.erb
|
|
@@ -175,6 +177,7 @@ files:
|
|
|
175
177
|
- app/components/keystone/ui/progress_component.rb
|
|
176
178
|
- app/components/keystone/ui/radio_card_component.html.erb
|
|
177
179
|
- app/components/keystone/ui/radio_card_component.rb
|
|
180
|
+
- app/components/keystone/ui/saved_layout.rb
|
|
178
181
|
- app/components/keystone/ui/section_component.html.erb
|
|
179
182
|
- app/components/keystone/ui/section_component.rb
|
|
180
183
|
- app/components/keystone/ui/select_component.html.erb
|
|
@@ -205,6 +208,8 @@ files:
|
|
|
205
208
|
- lib/keystone_ui/look_check.rb
|
|
206
209
|
- lib/keystone_ui/look_choice.rb
|
|
207
210
|
- lib/keystone_ui/missing_back_link.rb
|
|
211
|
+
- lib/keystone_ui/navigation_group.rb
|
|
212
|
+
- lib/keystone_ui/navigation_order.rb
|
|
208
213
|
- lib/keystone_ui/safelist.rb
|
|
209
214
|
- lib/keystone_ui/source_css.rb
|
|
210
215
|
- lib/keystone_ui/theme_choice.rb
|