keystone_ui 0.35.0 → 0.37.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: c2f3f4d0784444fc8cf7acde02c493fa2829dbcca367d1af4a852338b6f42212
4
- data.tar.gz: 366951b7d1e19d053229c9c509db6555a8de379442e7bf0d11dfb9637992e012
3
+ metadata.gz: f1534a3f721376c82b75b22a653e8bcee3a0509bcd3b09cd137c93c78e15110f
4
+ data.tar.gz: 0df6e2e29387563688ba68bc53d8c29dc69aa322aa86c84b6416c0e3af36f703
5
5
  SHA512:
6
- metadata.gz: 8f8064c88fc4ce2937ec84bff38ee5a60eab0fe03380995af9204fc2ccc58d82ec7c5cf89203ceb991088b0e0e06c6aa1b9d4106b251b099ea86e8303a686d85
7
- data.tar.gz: d9453549349d298e249bab69ee78ea54d70a09c1cbf989c9aaba21c307c8525b985708f4645e3374bb99e72457954cf9c5d39c4a5f71ab7ecbe6c0e4f490f17b
6
+ metadata.gz: 1869494c71af590cd1fa49fcd1062b024df02950f571e5dcbd7deaaa508574ad0f0aa2bfc8e23b0dc10d35ca440a50bc12e5d18063c62888de9eb29c42c09b45
7
+ data.tar.gz: 45563016cb89280c2e97cca008236c9fc50ba4681486a53f0d82fc4066e7db68368ad412c43b512cb2ca4ccd4f359dd29339890e24854eae2ead04ed9db7f6db
@@ -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
  }
@@ -5,17 +5,19 @@ module Keystone
5
5
  class Column
6
6
  attr_reader :key, :header_text
7
7
 
8
- def initialize(key, header_text, mobile_hidden: false, sortable: false, hideable: false)
8
+ def initialize(key, header_text, mobile_hidden: false, sortable: false, hideable: false, locked: false)
9
9
  @key = key
10
10
  @header_text = header_text
11
11
  @mobile_hidden = mobile_hidden
12
12
  @sortable = sortable
13
13
  @hideable = hideable
14
+ @locked = locked
14
15
  end
15
16
 
16
17
  def mobile_hidden? = @mobile_hidden
17
18
  def sortable? = @sortable
18
- def hideable? = @hideable
19
+ def hideable? = @hideable && !@locked
20
+ def locked? = @locked
19
21
  end
20
22
  end
21
23
  end
@@ -29,4 +29,5 @@
29
29
  </div>
30
30
  <% end %>
31
31
  </div>
32
+ <p data-column-picker-target="error" role="alert" class="<%= ERROR_CLASSES %>">Your column changes were not saved.</p>
32
33
  </div>
@@ -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
- @columns = columns
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
- @hidden_keys.include?(key.to_sym)
34
+ @layout.hidden?(key)
35
35
  end
36
36
 
37
37
  def option_classes(key)
@@ -17,6 +17,8 @@ module Keystone
17
17
  ROW_CLASSES_LAST = "ks-table-cell-last text-right text-sm whitespace-nowrap"
18
18
 
19
19
  MOBILE_HIDDEN_CLASSES = "hidden sm:table-cell"
20
+ HEADER_LOCKED_CLASSES = "ks-table-header-locked sticky left-0"
21
+ CELL_LOCKED_CLASSES = "ks-table-cell-locked sticky left-0"
20
22
 
21
23
  SORT_LINK_CLASSES = "ks-table-sort-link group inline-flex items-center"
22
24
  SORT_ICON_CLASSES = "h-4 w-4 flex-shrink-0"
@@ -30,8 +32,9 @@ module Keystone
30
32
  def initialize(items:, columns:, empty_message: nil, sort: nil, sort_direction: nil, sort_url: nil, hidden_columns: [], key: nil)
31
33
  @items = items.to_a
32
34
  @all_columns = columns.map { |col| normalize_column(col) }
33
- @hidden_columns = hidden_columns
34
- @columns = visible_columns(hidden_columns)
35
+ @default_hidden = hidden_columns
36
+ @layout = SavedLayout.new(columns: @all_columns, value: nil, default_hidden: hidden_columns)
37
+ @columns = @layout.visible_columns
35
38
  @key = key
36
39
  @empty_message = empty_message
37
40
  @sort = sort&.to_sym
@@ -61,7 +64,7 @@ module Keystone
61
64
  def column_picker
62
65
  return unless @save_url
63
66
 
64
- @column_picker ||= ColumnPickerComponent.new(columns: @all_columns, hidden_columns: @hidden_columns, save_url: @save_url)
67
+ @column_picker ||= ColumnPickerComponent.new(columns: @all_columns, layout: @layout, save_url: @save_url)
65
68
  end
66
69
 
67
70
  def column_keys
@@ -76,6 +79,7 @@ module Keystone
76
79
  cells = @columns.map.with_index do |column, index|
77
80
  tokens = [ header_classes_for(index) ]
78
81
  tokens << MOBILE_HIDDEN_CLASSES if column.mobile_hidden?
82
+ tokens << HEADER_LOCKED_CLASSES if index.zero? && column.locked?
79
83
 
80
84
  cell = {
81
85
  label: column.header_text,
@@ -119,6 +123,7 @@ module Keystone
119
123
  @columns.map.with_index do |column, index|
120
124
  tokens = [ row_classes_for(index) ]
121
125
  tokens << MOBILE_HIDDEN_CLASSES if column.mobile_hidden?
126
+ tokens << CELL_LOCKED_CLASSES if index.zero? && column.locked?
122
127
 
123
128
  cell = {
124
129
  value: resolve_value(item, column.key),
@@ -147,25 +152,11 @@ module Keystone
147
152
  saved = KeystoneUi.configuration.supplied_preference(helpers, @key)
148
153
  return unless saved
149
154
 
150
- @hidden_columns = saved[:value].to_h.fetch("hidden_columns", @hidden_columns)
151
- @all_columns = ordered_columns(saved[:value].to_h["column_order"])
152
- @columns = visible_columns(@hidden_columns)
155
+ @layout = SavedLayout.new(columns: @all_columns, value: saved[:value], default_hidden: @default_hidden)
156
+ @columns = @layout.visible_columns
153
157
  @save_url = saved[:save_url]
154
158
  end
155
159
 
156
- def ordered_columns(column_order)
157
- return @all_columns unless column_order
158
-
159
- positions = Array(column_order).map(&:to_sym).each_with_index.to_h
160
- hideable = @all_columns.select(&:hideable?).sort_by.with_index { |col, index| [ positions.fetch(col.key, positions.size + index) ] }
161
- @all_columns.map { |col| col.hideable? ? hideable.shift : col }
162
- end
163
-
164
- def visible_columns(hidden_columns)
165
- hidden_keys = Array(hidden_columns).map(&:to_sym).to_set
166
- @all_columns.reject { |col| col.hideable? && hidden_keys.include?(col.key) }
167
- end
168
-
169
160
  def visual_column_count
170
161
  @columns.length + (actions? ? 1 : 0)
171
162
  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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module KeystoneUi
4
- VERSION = "0.35.0"
4
+ VERSION = "0.37.0"
5
5
  end
@@ -240,16 +240,24 @@ outer element. See Conventions before using it.
240
240
  and the hideable ones fill the remaining places. With no `"column_order"` the
241
241
  columns keep their declared order. Whenever the supplier
242
242
  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, including
244
- for a person with nothing saved yet. When the
243
+ itself, aligned right, that saves to it as `ui_column_picker` does and shows
244
+ the same message when a save fails, including for a person with nothing saved
245
+ yet. That menu lists the hideable columns in
246
+ the order the table shows them and leaves ticked exactly the ones the table
247
+ shows. When the
245
248
  supplier returns nothing, when no supplier is set, or when no `key:` is
246
249
  passed, the table renders from `hidden_columns:` with no Columns menu. In
247
250
  every case `hidden_columns:` is the table's default layout.
248
- - `Keystone::Ui::Column.new(key, header_text, mobile_hidden: false, sortable: false, hideable: false)`
251
+ - `Keystone::Ui::Column.new(key, header_text, mobile_hidden: false, sortable: false, hideable: false, locked: false)`
249
252
  — a column with per-column options, for when a `{ key: "Label" }` hash is not
250
253
  enough. `mobile_hidden:` hides the column below `sm:`; `sortable:` opts it into
251
254
  sort headers; `hideable:` lets the Columns menu hide it and move it, and a
252
- saved `"column_order"` place it.
255
+ saved `"column_order"` place it. `locked: true` on the table's first column
256
+ keeps its header and cells in view while the rest of the table scrolls
257
+ sideways. A locked column that is not first renders as an ordinary column. A
258
+ locked column is never hideable, even with `hideable: true`: it is left out of
259
+ the Columns menu, `hidden_columns:` and a saved layout cannot hide it, and a
260
+ saved `"column_order"` cannot move it.
253
261
  - `ui_column_picker(columns:, hidden_columns: [], save_url: nil)` — a "Columns"
254
262
  dropdown with one row per `hideable` column, in the order `columns:` lists
255
263
  them. Each row has a checkbox and an up and a down button that move the
@@ -264,7 +272,11 @@ outer element. See Conventions before using it.
264
272
  `X-CSRF-Token` header, then reloads the page. A menu closed with nothing
265
273
  changed sends nothing. `column_order` lists every hideable column's key in
266
274
  the menu's order when it closes. With no `save_url:` it sends nothing. The app must provide that endpoint and persist
267
- both lists. The picker does not reorder the table: beside a table without
275
+ both lists. The endpoint must answer with a success status when it has saved
276
+ them. When it answers with an error status, or the request cannot reach the
277
+ server, the menu does not reload the page: it shows "Your column changes were
278
+ not saved." under the Columns button, and puts its boxes and its order back
279
+ to what the table shows. The picker does not reorder the table: beside a table without
268
280
  `key:`, the app must pass the table and the picker its columns in the saved
269
281
  order itself. A table
270
282
  given `key:` renders its own Columns menu when the supplier gives a save
@@ -540,8 +552,16 @@ outer element. See Conventions before using it.
540
552
 
541
553
  6. For a table, decide how columns are declared. Use `{ key: "Label" }` hashes
542
554
  when every column is plain. Switch the whole set to `Keystone::Ui::Column`
543
- objects as soon as one column needs `mobile_hidden:`, `sortable:`, or
544
- `hideable:`.
555
+ objects as soon as one column needs `mobile_hidden:`, `sortable:`,
556
+ `hideable:`, or `locked:`.
557
+
558
+ To keep a wide table's first column, such as a name, in view while the rest
559
+ scrolls sideways, declare it first with `locked: true`. Whether a table locks
560
+ its first column is the developer's choice, so ask rather than pick. If the
561
+ locked column stays in place but the scrolled columns show through its
562
+ cells, the app's keystone_ui-styles version does not define the
563
+ `ks-table-header-locked` and `ks-table-cell-locked` classes, so stop and hand
564
+ that part to `keystone_ui-install`, and add no classes to fix it.
545
565
 
546
566
  For a table whose hideable columns a user should be able to choose and keep,
547
567
  check whether the app's Keystone UI initializer sets a
@@ -127,6 +127,13 @@ holds the catalog.
127
127
  - **The last table column is right-aligned.** In a data table with no actions
128
128
  column, the last column's header is right-aligned like the cells beneath it,
129
129
  so a figure or total placed in that header lines up with the figures below.
130
+ - **A locked first column.** A table column can be marked locked. When it is
131
+ the table's first column, its header and cells stay in view while the rest
132
+ of the table scrolls sideways, styled by keystone_ui-styles' locked header
133
+ and cell classes. A locked column anywhere else renders as an ordinary
134
+ column. A locked column cannot be hidden or moved, so it never appears in the
135
+ "Columns" menu and a saved layout leaves it where it is, even when it is also
136
+ marked hideable.
130
137
  - **Saved table layouts.** A data table can be given a key naming it. A table
131
138
  with a key asks the app, or another gem such as keystone_ui-preferences, for
132
139
  the layout saved under that key for the request. A saved layout lists the
@@ -143,11 +150,15 @@ holds the catalog.
143
150
  table, at its right, and saves the user's choice there, even before anything
144
151
  has been saved. The menu lists the hideable columns in the order the table
145
152
  shows them, each with a box to show or hide it and buttons to move it up or
146
- down. An unticked column's name is greyed at once. Ticking boxes and moving
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
147
155
  columns sends nothing while the menu is open. Closing the menu, with its
148
156
  Columns button or by clicking anywhere outside it, saves the hidden columns
149
157
  and the order together once, then reloads the page. Closing it with nothing
150
- 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.
151
162
  A table with no key, or an app with no supplier, renders from its own call
152
163
  alone. Setting up the supplier belongs to the install local.
153
164
  - **Suggestions.** A form field can carry a list of suggested values. The
@@ -43,7 +43,7 @@ built on ViewComponent; hook it in before building any screen with those helpers
43
43
  ## How to use it
44
44
 
45
45
  1. Confirm the prerequisites: Ruby >= 3.2 and **tailwindcss-rails v4+** in the
46
- host app. The gem brings ViewComponent and keystone_ui-styles 0.11.0 or later with it.
46
+ host app. The gem brings ViewComponent and keystone_ui-styles 0.12.0 or later with it.
47
47
  Tailwind does not have to be initialized first, because the generator creates
48
48
  the stylesheet if it is missing.
49
49
 
@@ -248,6 +248,9 @@ built on ViewComponent; hook it in before building any screen with those helpers
248
248
  `"hidden_columns"`, such as `{ "hidden_columns" => ["sku"] }`. That list
249
249
  replaces the hidden columns the table's own call passes, and an empty list
250
250
  shows every column. Only columns marked hideable are hidden.
251
+ - A column marked locked is never hideable, even when it is also marked
252
+ hideable. A saved layout cannot hide it or move it, and the Columns menu
253
+ does not list it.
251
254
  - When `value` is `nil`, or has no `"hidden_columns"` string key, the table
252
255
  keeps the hidden columns its own call passes. A symbol key is not read, so
253
256
  `{ hidden_columns: [...] }` also keeps them.
@@ -279,6 +282,11 @@ built on ViewComponent; hook it in before building any screen with those helpers
279
282
  string keys, `"hidden_columns"` and `"column_order"`, or the saved order
280
283
  is not applied. With no `save_url`, the saved layout applies and no menu
281
284
  is shown.
285
+ - The action at `save_url` must answer a stored layout with a 2xx status.
286
+ Any other status, or a request that cannot reach the server, leaves the
287
+ page unreloaded. The table then shows "Your column changes were not
288
+ saved." beside its Columns button, and the menu's boxes and order go back
289
+ to what the table shows.
282
290
  - A companion preferences gem may set this supplier for the app. Ask the
283
291
  developer whether the app uses one, or which code stores each user's table
284
292
  layouts, before writing the callable.
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.35.0
4
+ version: 0.37.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tyler Schneider
@@ -175,6 +175,7 @@ files:
175
175
  - app/components/keystone/ui/progress_component.rb
176
176
  - app/components/keystone/ui/radio_card_component.html.erb
177
177
  - app/components/keystone/ui/radio_card_component.rb
178
+ - app/components/keystone/ui/saved_layout.rb
178
179
  - app/components/keystone/ui/section_component.html.erb
179
180
  - app/components/keystone/ui/section_component.rb
180
181
  - app/components/keystone/ui/select_component.html.erb