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:
Janne Valkealahti
2022-05-13 15:31:29 +01:00
parent 8477a5ac4c
commit d49429975d
6 changed files with 172 additions and 43 deletions

View File

@@ -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;
}

View File

@@ -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())) {

View File

@@ -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.
*

View File

@@ -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) {

View File

@@ -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");
}
}

View File

@@ -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();
}
}