Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

### Changed
- A data table and its Columns menu read a saved layout through one shared `Keystone::Ui::SavedLayout`, so they always agree on which columns are hidden and in what order. `ui_column_picker` still takes `hidden_columns:`.

## [0.36.0] - 2026-10-07

### Added
Expand Down
9 changes: 4 additions & 5 deletions app/components/keystone/ui/column_picker_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,17 @@ class ColumnPickerComponent < ViewComponent::Base

attr_reader :save_url

def initialize(columns:, hidden_columns: [], save_url: nil)
@columns = columns
@hidden_keys = Array(hidden_columns).map(&:to_sym).to_set
def initialize(columns:, hidden_columns: [], save_url: nil, layout: nil)
@layout = layout || SavedLayout.new(columns: columns, value: nil, default_hidden: hidden_columns)
@save_url = save_url
end

def hideable_columns
@columns.select(&:hideable?)
@layout.columns.select(&:hideable?)
end

def hidden?(key)
@hidden_keys.include?(key.to_sym)
@layout.hidden?(key)
end

def option_classes(key)
Expand Down
25 changes: 6 additions & 19 deletions app/components/keystone/ui/data_table_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ class DataTableComponent < ViewComponent::Base
def initialize(items:, columns:, empty_message: nil, sort: nil, sort_direction: nil, sort_url: nil, hidden_columns: [], key: nil)
@items = items.to_a
@all_columns = columns.map { |col| normalize_column(col) }
@hidden_columns = hidden_columns
@columns = visible_columns(hidden_columns)
@default_hidden = hidden_columns
@layout = SavedLayout.new(columns: @all_columns, value: nil, default_hidden: hidden_columns)
@columns = @layout.visible_columns
@key = key
@empty_message = empty_message
@sort = sort&.to_sym
Expand Down Expand Up @@ -63,7 +64,7 @@ def actions?
def column_picker
return unless @save_url

@column_picker ||= ColumnPickerComponent.new(columns: @all_columns, hidden_columns: @hidden_columns, save_url: @save_url)
@column_picker ||= ColumnPickerComponent.new(columns: @all_columns, layout: @layout, save_url: @save_url)
end

def column_keys
Expand Down Expand Up @@ -151,25 +152,11 @@ def apply_saved_layout
saved = KeystoneUi.configuration.supplied_preference(helpers, @key)
return unless saved

@hidden_columns = saved[:value].to_h.fetch("hidden_columns", @hidden_columns)
@all_columns = ordered_columns(saved[:value].to_h["column_order"])
@columns = visible_columns(@hidden_columns)
@layout = SavedLayout.new(columns: @all_columns, value: saved[:value], default_hidden: @default_hidden)
@columns = @layout.visible_columns
@save_url = saved[:save_url]
end

def ordered_columns(column_order)
return @all_columns unless column_order

positions = Array(column_order).map(&:to_sym).each_with_index.to_h
hideable = @all_columns.select(&:hideable?).sort_by.with_index { |col, index| [ positions.fetch(col.key, positions.size + index) ] }
@all_columns.map { |col| col.hideable? ? hideable.shift : col }
end

def visible_columns(hidden_columns)
hidden_keys = Array(hidden_columns).map(&:to_sym).to_set
@all_columns.reject { |col| col.hideable? && hidden_keys.include?(col.key) }
end

def visual_column_count
@columns.length + (actions? ? 1 : 0)
end
Expand Down
41 changes: 41 additions & 0 deletions app/components/keystone/ui/saved_layout.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# frozen_string_literal: true

module Keystone
module Ui
class SavedLayout
def initialize(columns:, value:, default_hidden:)
@columns = columns
@value = value.to_h
@default_hidden = default_hidden
end

def columns
@ordered_columns ||= ordered_columns
end

def visible_columns
columns.reject { |column| hidden?(column.key) }
end

def hidden?(key)
column = @columns.find { |col| col.key == key.to_sym }
column&.hideable? && hidden_keys.include?(key.to_sym)
end

private

def ordered_columns
column_order = @value["column_order"]
return @columns unless column_order

positions = Array(column_order).map(&:to_sym).each_with_index.to_h
hideable = @columns.select(&:hideable?).sort_by.with_index { |col, index| positions.fetch(col.key, positions.size + index) }
@columns.map { |col| col.hideable? ? hideable.shift : col }
end

def hidden_keys
@hidden_keys ||= Array(@value.fetch("hidden_columns", @default_hidden)).map(&:to_sym).to_set
end
end
end
end
8 changes: 8 additions & 0 deletions test/keystone/ui/column_picker_component_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

require "test_helper"
require_relative "../../../app/components/keystone/ui/column"
require_relative "../../../app/components/keystone/ui/saved_layout"
require_relative "../../../app/components/keystone/ui/column_picker_component"

class Keystone::Ui::ColumnPickerComponentTest < Minitest::Test
Expand Down Expand Up @@ -77,4 +78,11 @@ def test_checkbox_classes_render_the_ks_menu_checkbox_class
def test_checkbox_classes_hold_no_visual_utility
refute_match VISUAL_UTILITY, Keystone::Ui::ColumnPickerComponent::CHECKBOX_CLASSES
end

def test_given_a_saved_layout_lists_and_marks_its_columns_as_the_layout_reads_them
layout = Keystone::Ui::SavedLayout.new(columns: columns, value: { "hidden_columns" => [ "quantity" ], "column_order" => [ "price", "quantity" ] }, default_hidden: [])
component = Keystone::Ui::ColumnPickerComponent.new(columns: columns, layout: layout)

assert_equal [ [ :price, false ], [ :quantity, true ] ], component.hideable_columns.map { |col| [ col.key, component.hidden?(col.key) ] }
end
end
1 change: 1 addition & 0 deletions test/keystone/ui/data_table_component_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

require "test_helper"
require_relative "../../../app/components/keystone/ui/column"
require_relative "../../../app/components/keystone/ui/saved_layout"
require_relative "../../../app/components/keystone/ui/data_table_component"

class Keystone::Ui::DataTableComponentTest < Minitest::Test
Expand Down
33 changes: 33 additions & 0 deletions test/keystone/ui/saved_layout_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# frozen_string_literal: true

require "test_helper"
require_relative "../../../app/components/keystone/ui/column"
require_relative "../../../app/components/keystone/ui/saved_layout"

class Keystone::Ui::SavedLayoutTest < Minitest::Test
def columns
@columns ||= [
Keystone::Ui::Column.new(:month, "Month", locked: true),
Keystone::Ui::Column.new(:pipeline, "Pipeline", hideable: true),
Keystone::Ui::Column.new(:outreach, "Outreach", hideable: true)
]
end

def test_hides_the_hideable_columns_its_value_names
layout = Keystone::Ui::SavedLayout.new(columns: columns, value: { "hidden_columns" => [ "pipeline" ] }, default_hidden: [])

assert_equal [ :month, :outreach ], layout.visible_columns.map(&:key)
end

def test_hides_the_default_hidden_columns_when_nothing_is_saved
layout = Keystone::Ui::SavedLayout.new(columns: columns, value: nil, default_hidden: [ :outreach ])

assert_equal [ :month, :pipeline ], layout.visible_columns.map(&:key)
end

def test_puts_its_hideable_columns_in_the_saved_order_and_leaves_the_others_in_place
layout = Keystone::Ui::SavedLayout.new(columns: columns, value: { "column_order" => [ "outreach", "pipeline" ] }, default_hidden: [])

assert_equal [ :month, :outreach, :pipeline ], layout.columns.map(&:key)
end
end
4 changes: 3 additions & 1 deletion the_local/agents/keystone_ui-develop.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,9 @@ outer element. See Conventions before using it.
columns keep their declared order. Whenever the supplier
returns a save address, the table renders a "Columns" menu in a row above
itself, aligned right, that saves to it as `ui_column_picker` does, including
for a person with nothing saved yet. When the
for a person with nothing saved yet. That menu lists the hideable columns in
the order the table shows them and leaves ticked exactly the ones the table
shows. When the
supplier returns nothing, when no supplier is set, or when no `key:` is
passed, the table renders from `hidden_columns:` with no Columns menu. In
every case `hidden_columns:` is the table's default layout.
Expand Down
3 changes: 2 additions & 1 deletion the_local/agents/keystone_ui-info.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,8 @@ holds the catalog.
table, at its right, and saves the user's choice there, even before anything
has been saved. The menu lists the hideable columns in the order the table
shows them, each with a box to show or hide it and buttons to move it up or
down. An unticked column's name is greyed at once. Ticking boxes and moving
down. The menu and the table read the same saved layout, so the menu's
ticked boxes and order always match the columns the table shows. An unticked column's name is greyed at once. Ticking boxes and moving
columns sends nothing while the menu is open. Closing the menu, with its
Columns button or by clicking anywhere outside it, saves the hidden columns
and the order together once, then reloads the page. Closing it with nothing
Expand Down
Loading