Skip to content

Commit d494299

Browse files
committed
Expose default option in SingleItemSelector
- Tweak flow framework so that single item selector can be configured with a default item which is then "exposed" automatically so that user can just hit enter. - Fixes #414
1 parent 8477a5a commit d494299

File tree

6 files changed

+175
-46
lines changed

6 files changed

+175
-46
lines changed

spring-shell-core/src/main/java/org/springframework/shell/component/flow/BaseSingleItemSelector.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public abstract class BaseSingleItemSelector extends BaseInput<SingleItemSelecto
4141
private String resultValue;
4242
private ResultMode resultMode;
4343
private Map<String, String> selectItems = new HashMap<>();
44+
private String defaultSelect;
4445
private Comparator<SelectorItem<String>> comparator;
4546
private Function<SingleItemSelectorContext<String, SelectorItem<String>>, List<AttributedString>> renderer;
4647
private Integer maxItems;
@@ -84,6 +85,12 @@ public SingleItemSelectorSpec selectItems(Map<String, String> selectItems) {
8485
return this;
8586
}
8687

88+
@Override
89+
public SingleItemSelectorSpec defaultSelect(String name) {
90+
this.defaultSelect = name;
91+
return this;
92+
}
93+
8794
@Override
8895
public SingleItemSelectorSpec sort(Comparator<SelectorItem<String>> comparator) {
8996
this.comparator = comparator;
@@ -160,6 +167,10 @@ public Map<String, String> getSelectItems() {
160167
return selectItems;
161168
}
162169

170+
public String getDefaultSelect() {
171+
return defaultSelect;
172+
}
173+
163174
public Comparator<SelectorItem<String>> getComparator() {
164175
return comparator;
165176
}

spring-shell-core/src/main/java/org/springframework/shell/component/flow/ComponentFlow.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -555,8 +555,20 @@ private Stream<OrderedInputOperation> singleItemSelectorsStream() {
555555
List<SelectorItem<String>> selectorItems = input.getSelectItems().entrySet().stream()
556556
.map(e -> SelectorItem.of(e.getKey(), e.getValue()))
557557
.collect(Collectors.toList());
558+
559+
// setup possible item for initial expose
560+
String defaultSelect = input.getDefaultSelect();
561+
Stream<SelectorItem<String>> defaultCheckStream = StringUtils.hasText(defaultSelect)
562+
? selectorItems.stream()
563+
: Stream.empty();
564+
SelectorItem<String> defaultExpose = defaultCheckStream
565+
.filter(si -> ObjectUtils.nullSafeEquals(defaultSelect, si.getName()))
566+
.findFirst()
567+
.orElse(null);
568+
558569
SingleItemSelector<String, SelectorItem<String>> selector = new SingleItemSelector<>(terminal,
559570
selectorItems, input.getName(), input.getComparator());
571+
selector.setDefaultExpose(defaultExpose);
560572
Function<ComponentContext<?>, ComponentContext<?>> operation = (context) -> {
561573
if (input.getResultMode() == ResultMode.ACCEPT && input.isStoreResult()
562574
&& StringUtils.hasText(input.getResultValue())) {

spring-shell-core/src/main/java/org/springframework/shell/component/flow/SingleItemSelectorSpec.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,14 @@ public interface SingleItemSelectorSpec extends BaseInputSpec<SingleItemSelector
7676
*/
7777
SingleItemSelectorSpec selectItems(Map<String, String> selectItems);
7878

79+
/**
80+
* Automatically selects and exposes a given item.
81+
*
82+
* @param name the name
83+
* @return a builder
84+
*/
85+
SingleItemSelectorSpec defaultSelect(String name);
86+
7987
/**
8088
* Sets a {@link Comparator} for sorting items.
8189
*

spring-shell-core/src/main/java/org/springframework/shell/component/support/AbstractSelectorComponent.java

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.springframework.shell.component.context.ComponentContext;
3232
import org.springframework.shell.component.support.AbstractSelectorComponent.SelectorComponentContext;
3333
import org.springframework.util.Assert;
34+
import org.springframework.util.ObjectUtils;
3435
import org.springframework.util.StringUtils;
3536

3637
import static org.jline.keymap.KeyMap.ctrl;
@@ -54,6 +55,8 @@ public abstract class AbstractSelectorComponent<T, C extends SelectorComponentCo
5455
private boolean stale = false;
5556
private AtomicInteger start = new AtomicInteger(0);
5657
private AtomicInteger pos = new AtomicInteger(0);
58+
private I defaultExpose;
59+
private boolean expose = false;
5760

5861
public AbstractSelectorComponent(Terminal terminal, String name, List<I> items, boolean exitSelects,
5962
Comparator<I> comparator) {
@@ -95,6 +98,18 @@ public Function<T, String> getItemMapper() {
9598
return itemMapper;
9699
}
97100

101+
/**
102+
* Sets default expose item when component start.
103+
*
104+
* @param defaultExpose the default item
105+
*/
106+
public void setDefaultExpose(I defaultExpose) {
107+
this.defaultExpose = defaultExpose;
108+
if (defaultExpose != null) {
109+
expose = true;
110+
}
111+
}
112+
98113
/**
99114
* Gets items.
100115
*
@@ -120,6 +135,7 @@ protected void bindKeyMap(KeyMap<String> keyMap) {
120135
@Override
121136
protected C runInternal(C context) {
122137
C thisContext = getThisContext(context);
138+
initialExpose(thisContext);
123139
ItemStateViewProjection buildItemStateView = buildItemStateView(start.get(), thisContext);
124140
List<ItemState<I>> itemStateView = buildItemStateView.items;
125141
thisContext.setItemStateView(itemStateView);
@@ -224,6 +240,33 @@ else if (start.get() + pos.get() <= 0) {
224240
return false;
225241
}
226242

243+
private void initialExpose(C context) {
244+
if (!expose) {
245+
return;
246+
}
247+
expose = false;
248+
List<ItemState<I>> itemStates = context.getItemStates();
249+
if (itemStates == null) {
250+
AtomicInteger index = new AtomicInteger(0);
251+
itemStates = context.getItems().stream()
252+
.sorted(comparator)
253+
.map(item -> ItemState.of(item, item.getName(), index.getAndIncrement(), item.isEnabled()))
254+
.collect(Collectors.toList());
255+
}
256+
for (int i = 0; i < itemStates.size(); i++) {
257+
if (ObjectUtils.nullSafeEquals(itemStates.get(i).getName(), defaultExpose.getName())) {
258+
if (i < maxItems) {
259+
this.pos.set(i);
260+
}
261+
else {
262+
this.pos.set(maxItems - 1);
263+
this.start.set(i - maxItems + 1);
264+
}
265+
break;
266+
}
267+
}
268+
}
269+
227270
private ItemStateViewProjection buildItemStateView(int skip, SelectorComponentContext<T, I, ?> context) {
228271
List<ItemState<I>> itemStates = context.getItemStates();
229272
if (itemStates == null) {

spring-shell-core/src/test/java/org/springframework/shell/component/flow/ComponentFlowTests.java

Lines changed: 79 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -154,56 +154,89 @@ public void testSkipsGivenComponents() throws InterruptedException {
154154
assertThat(id4).containsExactlyInAnyOrder("value4");
155155
}
156156

157-
@Test
158-
public void testChoosesDynamically() throws InterruptedException {
159-
ComponentFlow wizard = ComponentFlow.builder()
157+
@Test
158+
public void testChoosesDynamically() throws InterruptedException {
159+
ComponentFlow wizard = ComponentFlow.builder()
160+
.terminal(getTerminal())
161+
.resourceLoader(getResourceLoader())
162+
.templateExecutor(getTemplateExecutor())
163+
.withStringInput("id1")
164+
.name("name")
165+
.next(ctx -> ctx.get("id1"))
166+
.and()
167+
.withStringInput("id2")
168+
.name("name")
169+
.resultValue("value2")
170+
.resultMode(ResultMode.ACCEPT)
171+
.next(ctx -> null)
172+
.and()
173+
.withStringInput("id3")
174+
.name("name")
175+
.resultValue("value3")
176+
.resultMode(ResultMode.ACCEPT)
177+
.next(ctx -> null)
178+
.and()
179+
.build();
180+
181+
ExecutorService service = Executors.newFixedThreadPool(1);
182+
CountDownLatch latch = new CountDownLatch(1);
183+
AtomicReference<ComponentFlowResult> result = new AtomicReference<>();
184+
service.execute(() -> {
185+
result.set(wizard.run());
186+
latch.countDown();
187+
});
188+
189+
// id1
190+
TestBuffer testBuffer = new TestBuffer().append("id3").cr();
191+
write(testBuffer.getBytes());
192+
// id3
193+
testBuffer = new TestBuffer().cr();
194+
write(testBuffer.getBytes());
195+
latch.await(4, TimeUnit.SECONDS);
196+
ComponentFlowResult inputWizardResult = result.get();
197+
assertThat(inputWizardResult).isNotNull();
198+
String id1 = inputWizardResult.getContext().get("id1");
199+
// TODO: should be able to check if variable exists
200+
// String id2 = inputWizardResult.getContext().get("id2");
201+
String id3 = inputWizardResult.getContext().get("id3");
202+
assertThat(id1).isEqualTo("id3");
203+
// assertThat(id2).isNull();
204+
assertThat(id3).isEqualTo("value3");
205+
}
206+
207+
@Test
208+
public void testAutoShowsDefault() throws InterruptedException {
209+
Map<String, String> single1SelectItems = new HashMap<>();
210+
single1SelectItems.put("key1", "value1");
211+
single1SelectItems.put("key2", "value2");
212+
ComponentFlow wizard = ComponentFlow.builder()
160213
.terminal(getTerminal())
161214
.resourceLoader(getResourceLoader())
162215
.templateExecutor(getTemplateExecutor())
163-
.withStringInput("id1")
164-
.name("name")
165-
.next(ctx -> ctx.get("id1"))
166-
.and()
167-
.withStringInput("id2")
168-
.name("name")
169-
.resultValue("value2")
170-
.resultMode(ResultMode.ACCEPT)
171-
.next(ctx -> null)
172-
.and()
173-
.withStringInput("id3")
174-
.name("name")
175-
.resultValue("value3")
176-
.resultMode(ResultMode.ACCEPT)
177-
.next(ctx -> null)
216+
.withSingleItemSelector("single1")
217+
.name("Single1")
218+
.selectItems(single1SelectItems)
219+
.defaultSelect("key2")
178220
.and()
179221
.build();
180222

181-
ExecutorService service = Executors.newFixedThreadPool(1);
182-
CountDownLatch latch = new CountDownLatch(1);
183-
AtomicReference<ComponentFlowResult> result = new AtomicReference<>();
184-
185-
service.execute(() -> {
186-
result.set(wizard.run());
187-
latch.countDown();
188-
});
189-
190-
// id1
191-
TestBuffer testBuffer = new TestBuffer().append("id3").cr();
192-
write(testBuffer.getBytes());
193-
194-
// id3
195-
testBuffer = new TestBuffer().cr();
196-
write(testBuffer.getBytes());
197-
198-
latch.await(4, TimeUnit.SECONDS);
199-
ComponentFlowResult inputWizardResult = result.get();
200-
assertThat(inputWizardResult).isNotNull();
201-
String id1 = inputWizardResult.getContext().get("id1");
202-
// TODO: should be able to check if variable exists
203-
// String id2 = inputWizardResult.getContext().get("id2");
204-
String id3 = inputWizardResult.getContext().get("id3");
205-
assertThat(id1).isEqualTo("id3");
206-
// assertThat(id2).isNull();
207-
assertThat(id3).isEqualTo("value3");
208-
}
223+
ExecutorService service = Executors.newFixedThreadPool(1);
224+
CountDownLatch latch = new CountDownLatch(1);
225+
AtomicReference<ComponentFlowResult> result = new AtomicReference<>();
226+
227+
service.execute(() -> {
228+
result.set(wizard.run());
229+
latch.countDown();
230+
});
231+
232+
TestBuffer testBuffer = new TestBuffer();
233+
testBuffer = new TestBuffer().cr();
234+
write(testBuffer.getBytes());
235+
236+
latch.await(4, TimeUnit.SECONDS);
237+
ComponentFlowResult inputWizardResult = result.get();
238+
assertThat(inputWizardResult).isNotNull();
239+
String single1 = inputWizardResult.getContext().get("single1");
240+
assertThat(single1).isEqualTo("value2");
241+
}
209242
}

spring-shell-samples/src/main/java/org/springframework/shell/samples/standard/ComponentFlowCommands.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,16 @@
1919
import java.util.HashMap;
2020
import java.util.List;
2121
import java.util.Map;
22+
import java.util.stream.Collectors;
23+
import java.util.stream.IntStream;
2224

2325
import org.springframework.beans.factory.annotation.Autowired;
2426
import org.springframework.shell.component.flow.ComponentFlow;
2527
import org.springframework.shell.component.flow.SelectItem;
2628
import org.springframework.shell.standard.AbstractShellComponent;
2729
import org.springframework.shell.standard.ShellComponent;
2830
import org.springframework.shell.standard.ShellMethod;
31+
import org.springframework.shell.standard.ShellOption;
2932

3033
@ShellComponent
3134
public class ComponentFlowCommands extends AbstractShellComponent {
@@ -90,4 +93,23 @@ public void conditional() {
9093
.build();
9194
flow.run();
9295
}
96+
97+
@ShellMethod(key = "flow autoselect", value = "Autoselect item", group = "Flow")
98+
public void autoselect(
99+
@ShellOption(defaultValue = "Field3") String defaultValue
100+
) {
101+
Map<String, String> single1SelectItems = IntStream.range(1, 10)
102+
.boxed()
103+
.collect(Collectors.toMap(i -> "Field" + i, i -> "field" + i));
104+
105+
ComponentFlow flow = componentFlowBuilder.clone().reset()
106+
.withSingleItemSelector("single1")
107+
.name("Single1")
108+
.selectItems(single1SelectItems)
109+
.defaultSelect(defaultValue)
110+
.sort((o1, o2) -> o1.getName().compareTo(o2.getName()))
111+
.and()
112+
.build();
113+
flow.run();
114+
}
93115
}

0 commit comments

Comments
 (0)