Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change multi_list to allow preserving choice order #198

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Change log

## [v0.23.2] - 2023-01-11

### Changed
* Change multi_select to allow preserve user choice ordering

## [v0.23.1] - 2021-04-17

### Changed
Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ Or install it yourself as:
* [2.6.3.7 :filter](#2637-filter)
* [2.6.3.8 :min](#2638-min)
* [2.6.3.9 :max](#2639-max)
* [2.6.3.10 :preserve_order](#26310-preserve_order)
* [2.6.4 enum_select](#264-enum_select)
* [2.6.4.1 :per_page](#2641-per_page)
* [2.6.4.1 :disabled](#2641-disabled)
Expand Down Expand Up @@ -1153,6 +1154,22 @@ prompt.multi_select("Select drinks?", choices, max: 3)
# ‣ ⬡ bourbon
```

#### 2.6.3.10 `:preserve_order`

To preserve the ordering of an user selections, use the `:preserve_order` option:

```ruby
choices = %w(vodka beer wine whisky bourbon)
prompt.multi_select("Select drinks?", choices, preserve_order: true)
# =>
# Select drinks? (max. 3) beer, vodka, whisky
# ⬢ vodka
# ⬢ beer
# ⬡ wine
# ⬢ whisky
# ‣ ⬡ bourbon
```

### 2.6.4 enum_select

In order to ask for standard selection from indexed list you can use `enum_select` and pass question together with possible choices:
Expand Down
20 changes: 16 additions & 4 deletions lib/tty/prompt/multi_list.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

require_relative "list"
require_relative "selected_choices"
require_relative "ordered_selected_choices"

module TTY
class Prompt
Expand All @@ -18,7 +19,8 @@ class MultiList < List
# @api public
def initialize(prompt, **options)
super
@selected = SelectedChoices.new
@preserve_order = options.fetch(:preserve_order, false)
@selected = selected_choices_klass.new
@help = options[:help]
@echo = options.fetch(:echo, true)
@min = options[:min]
Expand Down Expand Up @@ -71,7 +73,7 @@ def keyspace(*)
def keyctrl_a(*)
return if @max && @max < choices.size

@selected = SelectedChoices.new(choices.enabled, choices.enabled_indexes)
@selected = selected_choices_klass.new(choices.enabled, choices.enabled_indexes)
end

# Revert currently selected choices when Ctrl+I is pressed
Expand All @@ -84,7 +86,7 @@ def keyctrl_r(*)
acc << idx if !choice.disabled? && !@selected.include?(choice)
acc
end
@selected = SelectedChoices.new(choices.enabled - @selected.to_a, indexes)
@selected = selected_choices_klass.new(choices.enabled - @selected.to_a, indexes)
end

private
Expand All @@ -102,7 +104,7 @@ def setup_defaults
choices.index(choices.find_by(:name, d.to_s))
end
end
@selected = SelectedChoices.new(@choices.values_at(*default_indexes),
@selected = selected_choices_klass.new(@choices.values_at(*default_indexes),
default_indexes)

if @default.empty?
Expand Down Expand Up @@ -219,6 +221,16 @@ def render_menu

output.join
end

# Render either SelectedChoices or OrderedSelectedChoices based on preserve_order option
#
# @return [Class]
#
# @api private
def selected_choices_klass
return OrderedSelectedChoices if @preserve_order
SelectedChoices
end
end # MultiList
end # Prompt
end # TTY
71 changes: 71 additions & 0 deletions lib/tty/prompt/ordered_selected_choices.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# frozen_string_literal: true

module TTY
class Prompt
# @api private
class OrderedSelectedChoices
include Enumerable

attr_reader :size

# Create ordered selected choices
#
# @param [Array<Choice>] selected
# @param [Array<Integer>] indexes (ignored)
#
# @api public
def initialize(selected = [], _indexes = [])
@selected = selected
@size = @selected.size
end

# Clear ordered selected choices
#
# @api public
def clear
@selected.clear
@size = 0
end

# Iterate over ordered selected choices
#
# @api public
def each(&block)
return to_enum unless block_given?

@selected.each(&block)
end

# Insert choice at the end
#
# @param [Integer] index (ignored)
# @param [Choice] choice
#
# @api public
def insert(_index, choice)
@selected << choice
@size += 1
self
end

# Delete choice at index
#
# @return [Choice]
# the deleted choice
#
# @api public
def delete_at(index)
return nil if index < 0
return nil if index >= @size

choice = @selected.delete_at(index)
@size -= 1
choice
end

def find_index_by(&search)
(0...@size).bsearch(&search)
end
end # OrderedSelectedChoices
end # Prompt
end # TTY
2 changes: 1 addition & 1 deletion lib/tty/prompt/version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

module TTY
class Prompt
VERSION = "0.23.1"
VERSION = "0.23.2"
end # Prompt
end # TTY
30 changes: 30 additions & 0 deletions spec/unit/multi_select_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -905,4 +905,34 @@ def exit_message(prompt, choices)
expect(prompt.output.string).to eq(expected_output)
end
end

context "with :preserve_order" do
it "preserves user choice ordering" do
choices = %w[A B C]
prompt.on(:keypress) { |e|
prompt.trigger(:keyup) if e.value == "k"
prompt.trigger(:keydown) if e.value == "j"
}
prompt.input << " " << "j" << " " << "j" << " " << "k" << " " << " " << "\r"
prompt.input.rewind

value = prompt.multi_select("What letter?", choices, preserve_order: true, per_page: 100)
expect(value).to eq(%w[A C B])

expected_output =
output_helper("What letter?", choices, "A", [], init: true, preserve_order: true,
hint: "Press #{up_down} arrow to move, Space/Ctrl+A|R to select (all|rev) and Enter to finish") +
output_helper("What letter?", choices, "A", %w[A], preserve_order: true) +
output_helper("What letter?", choices, "B", %w[A], preserve_order: true) +
output_helper("What letter?", choices, "B", %w[A B], preserve_order: true) +
output_helper("What letter?", choices, "C", %w[A B], preserve_order: true) +
output_helper("What letter?", choices, "C", %w[A B C], preserve_order: true) +
output_helper("What letter?", choices, "B", %w[A B C], preserve_order: true) +
output_helper("What letter?", choices, "B", %w[A C], preserve_order: true) +
output_helper("What letter?", choices, "B", %w[A C B], preserve_order: true) +
exit_message("What letter?", %w[A C B])

expect(prompt.output.string).to eq(expected_output)
end
end
end
73 changes: 73 additions & 0 deletions spec/unit/ordered_selected_choices_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# frozen_string_literal: true

RSpec.describe TTY::Prompt::OrderedSelectedChoices do
it "inserts choices at the end" do
choices = %w[A B C D E F]
selected = described_class.new

expect(selected.to_a).to eq([])
expect(selected.size).to eq(0)

selected.insert(5, "F")
selected.insert(1, "B")
selected.insert(3, "D")
selected.insert(0, "A")
selected.insert(4, "E")
selected.insert(2, "C")

expect(selected.to_a).to eq(["F", "B", "D", "A", "E", "C"])
expect(selected.size).to eq(6)

expect(selected.delete_at(3)).to eq("A")
end

it "initializes with selected choices" do
choices = %w[A B C D E F]
selected = described_class.new(choices, (0...choices.size).to_a)

expect(selected.to_a).to eq(choices)
expect(selected.size).to eq(6)

choice = selected.delete_at(3)
expect(choice).to eq("D")

expect(selected.to_a).to eq(%w[A B C E F])
expect(selected.size).to eq(5)
end

it "inserts and deletes choices" do
selected = described_class.new

selected.insert(5, "F")
selected.insert(1, "B")
selected.insert(3, "D")
selected.insert(0, "A")

expect(selected.to_a).to eq(%w[F B D A])
expect(selected.size).to eq(4)

choice = selected.delete_at(2)
expect(choice).to eq("D")
expect(selected.to_a).to eq(%w[F B A])
expect(selected.size).to eq(3)

selected.insert(4, "E")
choice = selected.delete_at(-999)

expect(choice).to eq(nil)
expect(selected.to_a).to eq(%w[F B A E])
expect(selected.size).to eq(4)
end

it "clears choices" do
selected = described_class.new(%w[B D F])

expect(selected.to_a).to eq(%w[B D F])
expect(selected.size).to eq(3)

selected.clear

expect(selected.to_a).to eq([])
expect(selected.size).to eq(0)
end
end