#2183: dynamically generate form fields for selected commandlet - #2233
#2183: dynamically generate form fields for selected commandlet#2233Hiepiscus wants to merge 31 commits into
Conversation
…let-view' into 2182-basic-structure-for-commandlet-view
… selected commandlet
quando632
left a comment
There was a problem hiding this comment.
Nice split into PropertyFormFieldFactory, that keeps the controller readable. Checking KeywordProperty before BooleanProperty is easy to get wrong since one extends the other, and it is correct here. All three tasks from #2183 are covered.
One theme runs through the findings: runCommandlet executes a different order than AbstractIdeContext.applyAndRun (reset, assign, validate, run), while working on the commandlet singletons that CommandletManagerImpl shares with the CLI context.
Still open from the DoD checklist: the CHANGELOG.adoc entry, and tests. The gui module already has AppBaseTest and HeadlessApplicationTest, and PropertyFormFieldFactory is a static factory, so "property type maps to expected node type" should be cheap to assert.
| property) { | ||
| for (javafx.scene.Node child : hbox.getChildren()) { | ||
| if (child instanceof javafx.scene.control.TextField textField) { | ||
| property.setValueAsString(textField.getText(), context); |
There was a problem hiding this comment.
Blank fields are assigned as well, and ToolProperty.parse("") throws, so pressing Run on install without input ends in the default uncaught exception handler. Skipping blank input leaves the property unset, which is what the CLI does for a missing optional argument.
| property.setValueAsString(textField.getText(), context); | |
| String value = textField.getText(); | |
| if (!value.isBlank()) { | |
| property.assignValueAsString(value, this.context, this.selectedCommandlet); | |
| } |
| } | ||
| } | ||
|
|
||
| this.selectedCommandlet.run(); |
There was a problem hiding this comment.
Without validate() a missing required value reaches run(), for example create would start with an empty project name. validate() gives us the message to show instead. Needs imports of com.devonfw.tools.ide.validation.ValidationResult and com.devonfw.ide.gui.modal.IdeDialog.
| this.selectedCommandlet.run(); | |
| ValidationResult result = this.selectedCommandlet.validate(); | |
| if (!result.isValid()) { | |
| new IdeDialog(IdeDialog.AlertType.ERROR, result.getErrorMessage()).showAndWait(); | |
| return; | |
| } | |
| this.selectedCommandlet.run(); |
| } | ||
|
|
||
| this.selectedCommandlet.run(); | ||
| } |
There was a problem hiding this comment.
applyAndRun also calls ensureLicenseAgreement(cmd) and checks isIdeHomeRequired() and isIdeRootRequired() before running. Skipping the license gate means a tool can be installed from the GUI without the agreement the CLI asks for.
There was a problem hiding this comment.
Addressed. I added the validate() check before running the commandlet, but moved the validation logic into a dedicated method.
| Parent root = loader.load(); | ||
|
|
||
| Stage stage = (Stage) selectedProject.getScene().getWindow(); | ||
| stage.setScene(new Scene(root)); |
There was a problem hiding this comment.
Replacing the scene of the primary stage leaves no way back to the main view, so the app has to be restarted after opening the commandlet view. A back button in commandlet-view.fxml or a separate Stage for the view would solve it.
There was a problem hiding this comment.
Thanks for the suggestion. I've updated the navigation logic so that the commandlet view is displayed in the existing window by replacing the center content of the BorderPane. A back button was added to the commandlet view, allowing users to return to the main view without restarting the application.
| } | ||
| } | ||
|
|
||
| this.selectedCommandlet.run(); |
There was a problem hiding this comment.
The result of the run never reaches the view. A success writes everything to the console and leaves the window unchanged, and a failure escapes as an uncaught exception, for example build outside a project throws CliException: Could not find build descriptor straight out of the handler. Wrapping run() in a try/catch and reporting both outcomes in the view would close the loop.
| commandletSelector.getItems().clear(); | ||
| commandletSelector.getItems().addAll(context.getCommandletManager().getCommandlets().stream() | ||
| .map(Commandlet::getName) | ||
| .toList()); |
There was a problem hiding this comment.
nit: .sorted() was dropped in 8f8f191, so the combo box lists about 40 commandlets unsorted. Was that intentional?
There was a problem hiding this comment.
The removal was intentional. I was considering a future approach where frequently used commandlets would be shown first. However, the current unsorted order is not predictable, so I've added .sorted() back for now.
| return name; | ||
| } | ||
|
|
||
| if (name.isEmpty() && alias != null) { |
There was a problem hiding this comment.
nit: the non option branch reimplements Property#getNameOrAlias().
Co-authored-by: quando632 <quang-hieu.do@capgemini.com>
Co-authored-by: quando632 <quang-hieu.do@capgemini.com>
Co-authored-by: quando632 <quang-hieu.do@capgemini.com>
Co-authored-by: quando632 <quang-hieu.do@capgemini.com>
pane with back button
quando632
left a comment
There was a problem hiding this comment.
Thanks for the thorough round, that covers almost everything. Pulling the checks into a separate validate method reads well, and routing the back navigation through rootPane.setCenter instead of swapping the scene is better than what I suggested, since the navigation stays in place. The tests and the CHANGELOG entry close both open DoD items.
Two of the new checks have side effects that I would fix before merging, and one point from the last round is still open.
| <HBox> | ||
| <children> | ||
| <Button fx:id="commandletOpen" disable="true" text="Commandlets" onAction="#openCommandlet"> | ||
| <HBox.margin> | ||
| <Insets bottom="10.0" left="10.0" right="10.0" top="10.0"/> | ||
| </HBox.margin> | ||
| </Button> | ||
| </children> | ||
| </HBox> |
There was a problem hiding this comment.
App.java:59 pins the minimum window size to half the screen, and at that size the tile list already fills the center, so this button is off screen and the feature cannot be reached without enlarging the window. The left navigation has room and is where the rest of the navigation lives.
Removing it here and adding this after the language block in the left navigation (around line 56) would do it, together with commandlets=Commandlets in nls/messages.properties and nls/messages_de.properties:
<VBox maxHeight="-Infinity" prefWidth="100.0" styleClass="sideNavigationElement">
<children>
<Button fx:id="commandletOpen" disable="true" maxWidth="1.7976931348623157E308" mnemonicParsing="false"
onAction="#openCommandlet" text="%commandlets"/>
</children>
<padding>
<Insets left="8.0" right="8.0"/>
</padding>
</VBox>| runButton.setDisable(true); | ||
| try { | ||
| this.selectedCommandlet.run(); | ||
| new IdeDialog(AlertType.INFORMATION, "Commandlet executed successfully.").showAndWait(); | ||
| } catch (Exception e) { | ||
| LOG.error("Commandlet execution failed", e); | ||
| new IdeDialog(IdeDialog.AlertType.ERROR, e.getMessage()).showAndWait(); | ||
| } finally { | ||
| runButton.setDisable(false); | ||
| } |
There was a problem hiding this comment.
run() blocks the JavaFX application thread, so the button never repaints as disabled and the clicks that arrive during the run are delivered right after the finally. A Task makes the disable effective and keeps the window responsive. Needs import javafx.concurrent.Task;.
| runButton.setDisable(true); | |
| try { | |
| this.selectedCommandlet.run(); | |
| new IdeDialog(AlertType.INFORMATION, "Commandlet executed successfully.").showAndWait(); | |
| } catch (Exception e) { | |
| LOG.error("Commandlet execution failed", e); | |
| new IdeDialog(IdeDialog.AlertType.ERROR, e.getMessage()).showAndWait(); | |
| } finally { | |
| runButton.setDisable(false); | |
| } | |
| Commandlet commandlet = this.selectedCommandlet; | |
| Task<Void> execution = new Task<>() { | |
| @Override | |
| protected Void call() { | |
| commandlet.run(); | |
| return null; | |
| } | |
| }; | |
| execution.setOnSucceeded(event -> { | |
| this.runButton.setDisable(false); | |
| new IdeDialog(AlertType.INFORMATION, "Commandlet executed successfully.").showAndWait(); | |
| }); | |
| execution.setOnFailed(event -> { | |
| this.runButton.setDisable(false); | |
| Throwable error = execution.getException(); | |
| LOG.error("Commandlet execution failed", error); | |
| new IdeDialog(IdeDialog.AlertType.ERROR, error.getMessage()).showAndWait(); | |
| }); | |
| this.runButton.setDisable(true); | |
| Thread thread = new Thread(execution, "commandlet-" + commandlet.getName()); | |
| thread.setDaemon(true); | |
| thread.start(); |
| <ScrollPane fitToWidth="true" VBox.vgrow="ALWAYS"> | ||
| <VBox fx:id="formContainer" spacing="5.0"/> | ||
| </ScrollPane> | ||
| <Button fx:id="runButton" alignment="BOTTOM_RIGHT" contentDisplay="BOTTOM" mnemonicParsing="false" onAction="#runCommandlet" text="Run"/> |
There was a problem hiding this comment.
alignment on a Button aligns the content inside the button, not the button within its parent, so Run currently sits at the bottom left. A right aligned container puts it where the attribute intends.
| <Button fx:id="runButton" alignment="BOTTOM_RIGHT" contentDisplay="BOTTOM" mnemonicParsing="false" onAction="#runCommandlet" text="Run"/> | |
| <HBox alignment="CENTER_RIGHT"> | |
| <children> | |
| <Button fx:id="runButton" mnemonicParsing="false" onAction="#runCommandlet" text="%run"/> | |
| </children> | |
| </HBox> |
Co-authored-by: quando632 <quang-hieu.do@capgemini.com>
….java Co-authored-by: quando632 <quang-hieu.do@capgemini.com>
Co-authored-by: quando632 <quang-hieu.do@capgemini.com>
Co-authored-by: quando632 <quang-hieu.do@capgemini.com>
Co-authored-by: quando632 <quang-hieu.do@capgemini.com>
This PR fixes #2183
Introduces dynamic form generation for commandlets. Instead of using a static input field, the UI now creates appropriate form fields based on the properties of the selected commandlet for a more flexible configuration experience.
Show more lines
Implemented changes:
PropertyFormFieldFactory(new): create appropriate UI based on commandlet:TextFieldfor positional parameters and named optionsCheckBoxfor boolean flagsTextFieldwith aScrollPane+VBoxin the FXML layoutonCommandletSelected()listener to regenerate the form on selection changerunCommandlet()to read property values from generated form fieldsTesting instructions
Please add conscise, understandable instructions on how a reviewer can test/verify the functionality of your contribution here:
AppLauncherCommandletsRunChecklist for this PR
Make sure everything is checked before merging this PR. For further info please also see
our DoD.
mvn clean testlocally all tests pass and build is successful#«issue-id»: «brief summary»(e.g.#921: fixed setup.bat). If no issue ID exists, title only.In Progressand assigned to you or there is no issue (might happen for very small PRs)with
internal