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
This commit is contained in:
@@ -41,6 +41,7 @@ public abstract class BaseSingleItemSelector extends BaseInput<SingleItemSelecto
|
||||
private String resultValue;
|
||||
private ResultMode resultMode;
|
||||
private Map<String, String> selectItems = new HashMap<>();
|
||||
private String defaultSelect;
|
||||
private Comparator<SelectorItem<String>> comparator;
|
||||
private Function<SingleItemSelectorContext<String, SelectorItem<String>>, List<AttributedString>> renderer;
|
||||
private Integer maxItems;
|
||||
@@ -84,6 +85,12 @@ public abstract class BaseSingleItemSelector extends BaseInput<SingleItemSelecto
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SingleItemSelectorSpec defaultSelect(String name) {
|
||||
this.defaultSelect = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SingleItemSelectorSpec sort(Comparator<SelectorItem<String>> comparator) {
|
||||
this.comparator = comparator;
|
||||
@@ -160,6 +167,10 @@ public abstract class BaseSingleItemSelector extends BaseInput<SingleItemSelecto
|
||||
return selectItems;
|
||||
}
|
||||
|
||||
public String getDefaultSelect() {
|
||||
return defaultSelect;
|
||||
}
|
||||
|
||||
public Comparator<SelectorItem<String>> getComparator() {
|
||||
return comparator;
|
||||
}
|
||||
|
||||
@@ -555,8 +555,20 @@ public interface ComponentFlow {
|
||||
List<SelectorItem<String>> selectorItems = input.getSelectItems().entrySet().stream()
|
||||
.map(e -> SelectorItem.of(e.getKey(), e.getValue()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// setup possible item for initial expose
|
||||
String defaultSelect = input.getDefaultSelect();
|
||||
Stream<SelectorItem<String>> defaultCheckStream = StringUtils.hasText(defaultSelect)
|
||||
? selectorItems.stream()
|
||||
: Stream.empty();
|
||||
SelectorItem<String> defaultExpose = defaultCheckStream
|
||||
.filter(si -> ObjectUtils.nullSafeEquals(defaultSelect, si.getName()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
|
||||
SingleItemSelector<String, SelectorItem<String>> selector = new SingleItemSelector<>(terminal,
|
||||
selectorItems, input.getName(), input.getComparator());
|
||||
selector.setDefaultExpose(defaultExpose);
|
||||
Function<ComponentContext<?>, ComponentContext<?>> operation = (context) -> {
|
||||
if (input.getResultMode() == ResultMode.ACCEPT && input.isStoreResult()
|
||||
&& StringUtils.hasText(input.getResultValue())) {
|
||||
|
||||
@@ -76,6 +76,14 @@ public interface SingleItemSelectorSpec extends BaseInputSpec<SingleItemSelector
|
||||
*/
|
||||
SingleItemSelectorSpec selectItems(Map<String, String> selectItems);
|
||||
|
||||
/**
|
||||
* Automatically selects and exposes a given item.
|
||||
*
|
||||
* @param name the name
|
||||
* @return a builder
|
||||
*/
|
||||
SingleItemSelectorSpec defaultSelect(String name);
|
||||
|
||||
/**
|
||||
* Sets a {@link Comparator} for sorting items.
|
||||
*
|
||||
|
||||
@@ -31,6 +31,7 @@ import org.springframework.shell.component.context.BaseComponentContext;
|
||||
import org.springframework.shell.component.context.ComponentContext;
|
||||
import org.springframework.shell.component.support.AbstractSelectorComponent.SelectorComponentContext;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import static org.jline.keymap.KeyMap.ctrl;
|
||||
@@ -54,6 +55,8 @@ public abstract class AbstractSelectorComponent<T, C extends SelectorComponentCo
|
||||
private boolean stale = false;
|
||||
private AtomicInteger start = new AtomicInteger(0);
|
||||
private AtomicInteger pos = new AtomicInteger(0);
|
||||
private I defaultExpose;
|
||||
private boolean expose = false;
|
||||
|
||||
public AbstractSelectorComponent(Terminal terminal, String name, List<I> items, boolean exitSelects,
|
||||
Comparator<I> comparator) {
|
||||
@@ -95,6 +98,18 @@ public abstract class AbstractSelectorComponent<T, C extends SelectorComponentCo
|
||||
return itemMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets default expose item when component start.
|
||||
*
|
||||
* @param defaultExpose the default item
|
||||
*/
|
||||
public void setDefaultExpose(I defaultExpose) {
|
||||
this.defaultExpose = defaultExpose;
|
||||
if (defaultExpose != null) {
|
||||
expose = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets items.
|
||||
*
|
||||
@@ -120,6 +135,7 @@ public abstract class AbstractSelectorComponent<T, C extends SelectorComponentCo
|
||||
@Override
|
||||
protected C runInternal(C context) {
|
||||
C thisContext = getThisContext(context);
|
||||
initialExpose(thisContext);
|
||||
ItemStateViewProjection buildItemStateView = buildItemStateView(start.get(), thisContext);
|
||||
List<ItemState<I>> itemStateView = buildItemStateView.items;
|
||||
thisContext.setItemStateView(itemStateView);
|
||||
@@ -224,6 +240,33 @@ public abstract class AbstractSelectorComponent<T, C extends SelectorComponentCo
|
||||
return false;
|
||||
}
|
||||
|
||||
private void initialExpose(C context) {
|
||||
if (!expose) {
|
||||
return;
|
||||
}
|
||||
expose = false;
|
||||
List<ItemState<I>> itemStates = context.getItemStates();
|
||||
if (itemStates == null) {
|
||||
AtomicInteger index = new AtomicInteger(0);
|
||||
itemStates = context.getItems().stream()
|
||||
.sorted(comparator)
|
||||
.map(item -> ItemState.of(item, item.getName(), index.getAndIncrement(), item.isEnabled()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
for (int i = 0; i < itemStates.size(); i++) {
|
||||
if (ObjectUtils.nullSafeEquals(itemStates.get(i).getName(), defaultExpose.getName())) {
|
||||
if (i < maxItems) {
|
||||
this.pos.set(i);
|
||||
}
|
||||
else {
|
||||
this.pos.set(maxItems - 1);
|
||||
this.start.set(i - maxItems + 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ItemStateViewProjection buildItemStateView(int skip, SelectorComponentContext<T, I, ?> context) {
|
||||
List<ItemState<I>> itemStates = context.getItemStates();
|
||||
if (itemStates == null) {
|
||||
|
||||
@@ -154,56 +154,89 @@ public class ComponentFlowTests extends AbstractShellTests {
|
||||
assertThat(id4).containsExactlyInAnyOrder("value4");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChoosesDynamically() throws InterruptedException {
|
||||
ComponentFlow wizard = ComponentFlow.builder()
|
||||
@Test
|
||||
public void testChoosesDynamically() throws InterruptedException {
|
||||
ComponentFlow wizard = ComponentFlow.builder()
|
||||
.terminal(getTerminal())
|
||||
.resourceLoader(getResourceLoader())
|
||||
.templateExecutor(getTemplateExecutor())
|
||||
.withStringInput("id1")
|
||||
.name("name")
|
||||
.next(ctx -> ctx.get("id1"))
|
||||
.and()
|
||||
.withStringInput("id2")
|
||||
.name("name")
|
||||
.resultValue("value2")
|
||||
.resultMode(ResultMode.ACCEPT)
|
||||
.next(ctx -> null)
|
||||
.and()
|
||||
.withStringInput("id3")
|
||||
.name("name")
|
||||
.resultValue("value3")
|
||||
.resultMode(ResultMode.ACCEPT)
|
||||
.next(ctx -> null)
|
||||
.and()
|
||||
.build();
|
||||
|
||||
ExecutorService service = Executors.newFixedThreadPool(1);
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
AtomicReference<ComponentFlowResult> result = new AtomicReference<>();
|
||||
service.execute(() -> {
|
||||
result.set(wizard.run());
|
||||
latch.countDown();
|
||||
});
|
||||
|
||||
// id1
|
||||
TestBuffer testBuffer = new TestBuffer().append("id3").cr();
|
||||
write(testBuffer.getBytes());
|
||||
// id3
|
||||
testBuffer = new TestBuffer().cr();
|
||||
write(testBuffer.getBytes());
|
||||
latch.await(4, TimeUnit.SECONDS);
|
||||
ComponentFlowResult inputWizardResult = result.get();
|
||||
assertThat(inputWizardResult).isNotNull();
|
||||
String id1 = inputWizardResult.getContext().get("id1");
|
||||
// TODO: should be able to check if variable exists
|
||||
// String id2 = inputWizardResult.getContext().get("id2");
|
||||
String id3 = inputWizardResult.getContext().get("id3");
|
||||
assertThat(id1).isEqualTo("id3");
|
||||
// assertThat(id2).isNull();
|
||||
assertThat(id3).isEqualTo("value3");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAutoShowsDefault() throws InterruptedException {
|
||||
Map<String, String> single1SelectItems = new HashMap<>();
|
||||
single1SelectItems.put("key1", "value1");
|
||||
single1SelectItems.put("key2", "value2");
|
||||
ComponentFlow wizard = ComponentFlow.builder()
|
||||
.terminal(getTerminal())
|
||||
.resourceLoader(getResourceLoader())
|
||||
.templateExecutor(getTemplateExecutor())
|
||||
.withStringInput("id1")
|
||||
.name("name")
|
||||
.next(ctx -> ctx.get("id1"))
|
||||
.and()
|
||||
.withStringInput("id2")
|
||||
.name("name")
|
||||
.resultValue("value2")
|
||||
.resultMode(ResultMode.ACCEPT)
|
||||
.next(ctx -> null)
|
||||
.and()
|
||||
.withStringInput("id3")
|
||||
.name("name")
|
||||
.resultValue("value3")
|
||||
.resultMode(ResultMode.ACCEPT)
|
||||
.next(ctx -> null)
|
||||
.withSingleItemSelector("single1")
|
||||
.name("Single1")
|
||||
.selectItems(single1SelectItems)
|
||||
.defaultSelect("key2")
|
||||
.and()
|
||||
.build();
|
||||
|
||||
ExecutorService service = Executors.newFixedThreadPool(1);
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
AtomicReference<ComponentFlowResult> result = new AtomicReference<>();
|
||||
ExecutorService service = Executors.newFixedThreadPool(1);
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
AtomicReference<ComponentFlowResult> result = new AtomicReference<>();
|
||||
|
||||
service.execute(() -> {
|
||||
result.set(wizard.run());
|
||||
latch.countDown();
|
||||
});
|
||||
service.execute(() -> {
|
||||
result.set(wizard.run());
|
||||
latch.countDown();
|
||||
});
|
||||
|
||||
// id1
|
||||
TestBuffer testBuffer = new TestBuffer().append("id3").cr();
|
||||
write(testBuffer.getBytes());
|
||||
TestBuffer testBuffer = new TestBuffer();
|
||||
testBuffer = new TestBuffer().cr();
|
||||
write(testBuffer.getBytes());
|
||||
|
||||
// id3
|
||||
testBuffer = new TestBuffer().cr();
|
||||
write(testBuffer.getBytes());
|
||||
|
||||
latch.await(4, TimeUnit.SECONDS);
|
||||
ComponentFlowResult inputWizardResult = result.get();
|
||||
assertThat(inputWizardResult).isNotNull();
|
||||
String id1 = inputWizardResult.getContext().get("id1");
|
||||
// TODO: should be able to check if variable exists
|
||||
// String id2 = inputWizardResult.getContext().get("id2");
|
||||
String id3 = inputWizardResult.getContext().get("id3");
|
||||
assertThat(id1).isEqualTo("id3");
|
||||
// assertThat(id2).isNull();
|
||||
assertThat(id3).isEqualTo("value3");
|
||||
}
|
||||
latch.await(4, TimeUnit.SECONDS);
|
||||
ComponentFlowResult inputWizardResult = result.get();
|
||||
assertThat(inputWizardResult).isNotNull();
|
||||
String single1 = inputWizardResult.getContext().get("single1");
|
||||
assertThat(single1).isEqualTo("value2");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.shell.component.flow.ComponentFlow;
|
||||
@@ -26,6 +28,7 @@ import org.springframework.shell.component.flow.SelectItem;
|
||||
import org.springframework.shell.standard.AbstractShellComponent;
|
||||
import org.springframework.shell.standard.ShellComponent;
|
||||
import org.springframework.shell.standard.ShellMethod;
|
||||
import org.springframework.shell.standard.ShellOption;
|
||||
|
||||
@ShellComponent
|
||||
public class ComponentFlowCommands extends AbstractShellComponent {
|
||||
@@ -90,4 +93,23 @@ public class ComponentFlowCommands extends AbstractShellComponent {
|
||||
.build();
|
||||
flow.run();
|
||||
}
|
||||
|
||||
@ShellMethod(key = "flow autoselect", value = "Autoselect item", group = "Flow")
|
||||
public void autoselect(
|
||||
@ShellOption(defaultValue = "Field3") String defaultValue
|
||||
) {
|
||||
Map<String, String> single1SelectItems = IntStream.range(1, 10)
|
||||
.boxed()
|
||||
.collect(Collectors.toMap(i -> "Field" + i, i -> "field" + i));
|
||||
|
||||
ComponentFlow flow = componentFlowBuilder.clone().reset()
|
||||
.withSingleItemSelector("single1")
|
||||
.name("Single1")
|
||||
.selectItems(single1SelectItems)
|
||||
.defaultSelect(defaultValue)
|
||||
.sort((o1, o2) -> o1.getName().compareTo(o2.getName()))
|
||||
.and()
|
||||
.build();
|
||||
flow.run();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user