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

[ui] Allow to setup the auxclasspath in the designer #1159

Merged
merged 20 commits into from Jun 8, 2018

Conversation

3 participants
@akshatbahety
Contributor

akshatbahety commented May 30, 2018

Setting up of auxClassPath using a UI component within the designer.

@oowekyala oowekyala changed the title from auxClassPath UI to [ui] Allow to setup the auxclasspath in the designer May 30, 2018

@oowekyala oowekyala self-assigned this May 30, 2018

@oowekyala oowekyala added this to the 6.5.0 milestone May 30, 2018

@oowekyala

I'll push the fix to your bug on your branch if I can

@@ -111,6 +115,8 @@
private SourceEditorController sourceEditorController;
@FXML
private EventLogController eventLogPanelController;
@FXML

This comment has been minimized.

@oowekyala

oowekyala May 30, 2018

Member

This should not be annotated @FXML. The annotation is only for fields that should be injected by the FxmlLoader. The other controllers are injected because their FXML fragment is included in designer.fxml. Here you must create a new controller yourself. I'll push to your branch to show you

}
private void fileSeleced() {

This comment has been minimized.

@oowekyala
}
public void showAuxPathWizard() throws Exception {

This comment has been minimized.

@oowekyala

oowekyala May 30, 2018

Member

Encapsulating this logic here is actually a very nice idea. I'll refactor the other instances of this problem to follow that design principle

prefWidth="172.0">
<columns>
<TableColumn fx:id="fileList" prefWidth="171.0" text="Files Selected"/>
</columns>

This comment has been minimized.

@oowekyala

oowekyala May 30, 2018

Member

The file name is wrong

The controller is the Java object of type XXXController. It adds logic to the UI. The FXML file doesn't control anything, instead it is controlled by the controller. It's the View in the Model-View-Controller pattern. This is a pretty basic concept which you should be aware of, see here and on the wikipedia page for example.

Consider naming it auxclasspath-setup-popup.fxml for example

Also, no offence but the ui is substandard for now

  • Your TitledPane should not be collapsible.
  • You've hardcoded every width and height. You should rather use anchorpane constraints to have nodes fit their parent. Otherwise resizing the popup is just hell. As a matter of fact, most of the time the only width and height you should explictly set is that of the root. Your labels don't fit their text because of that.
  • Why do you need two listviews ? One is enough. Allow items to be selected and have a remove button available when your selection is non empty.
@@ -42,6 +44,12 @@
<MenuItem fx:id="licenseMenuItem" mnemonicParsing="false" text="License" />
</items>
</Menu>
<Menu mnemonicParsing="false" text="PMD Configuration">
<items>
<MenuItem fx:id="pmdconfig" mnemonicParsing="false" text="AuxClassPath SetUp"/>

This comment has been minimized.

@oowekyala

oowekyala May 30, 2018

Member

The naming here is sloppy too.

text="AuxClassPath SetUp"

  • The capitalisation is wrong
  • All other menu items are named with the "verb + object" convention, eg "Open file" or "Export rule". This menu item should follow the same convention, e.g. be named "Set up the auxclasspath".

fx:id="pmdconfig"

You're using the name of the Menu as the id of the MenuItem. FX ids also have a naming convention, that is, they should be camelCase, and be suffixed with their type. You can see eg licenseMenuItem a few lines above. Rename that to setupAuxclasspathMenuItem

Conventions are important, and even though they're usually implicit, you can usually pick them up just by looking around

private void fileSeleced() {
FileChooser chooser = new FileChooser();

This comment has been minimized.

@oowekyala

oowekyala May 30, 2018

Member

You should add an extension filter to only allow selecting jar files. There's an example here.

private void fileSeleced() {
FileChooser chooser = new FileChooser();
chooser.setTitle("Load source from file");

This comment has been minimized.

@oowekyala

oowekyala May 30, 2018

Member

"Load source from file"? That's a copy-paste error

@oowekyala oowekyala added the in:ui label May 30, 2018

private void fileSeleced() {
FileChooser chooser = new FileChooser();
chooser.setTitle("Load source from file");
File file = chooser.showOpenDialog(designerRoot.getMainStage());

This comment has been minimized.

@oowekyala

oowekyala May 30, 2018

Member

Also, you should use showOpenMultipleDialog

This comment has been minimized.

@jsotuyod

jsotuyod May 30, 2018

Member

and don't forget to filter file types:

            chooser.getExtensionFilters().addAll(
                new FileChooser.ExtensionFilter("Java JARs", "*.jar"),
                new FileChooser.ExtensionFilter("Java WARs", "*.war"),
                new FileChooser.ExtensionFilter("Java EARs", "*.ear"),
                new FileChooser.ExtensionFilter("Java class files", "*.class"),
            );
});
Parent root1 = fxmlLoader.load();
Stage stage = new Stage();

This comment has been minimized.

@oowekyala

oowekyala May 30, 2018

Member

And your stage should be modal

*/
package net.sourceforge.pmd.util.fxdesigner.util;

This comment has been minimized.

@jsotuyod

jsotuyod May 30, 2018

Member

I don't think this package is the right one for this... we don't have any controllers in it, and controllers are not utilities... I'd move this up to net.sourceforge.pmd.util.fxdesigner or maybe even create a separate package for controllers that are modals or split by screen... @oowekyala ?

This comment has been minimized.

@oowekyala

oowekyala May 30, 2018

Member

Oh yes good catch

Yeah making a new package for popups and such is a good idea.

private void fileSeleced() {
FileChooser chooser = new FileChooser();
chooser.setTitle("Load source from file");
File file = chooser.showOpenDialog(designerRoot.getMainStage());

This comment has been minimized.

@jsotuyod

jsotuyod May 30, 2018

Member

and don't forget to filter file types:

            chooser.getExtensionFilters().addAll(
                new FileChooser.ExtensionFilter("Java JARs", "*.jar"),
                new FileChooser.ExtensionFilter("Java WARs", "*.war"),
                new FileChooser.ExtensionFilter("Java EARs", "*.ear"),
                new FileChooser.ExtensionFilter("Java class files", "*.class"),
            );
Updated the UI (still some work left) , Working on the fileNames in t…
…he list view , about the UI I think this shouldn't be scalable as the components would be just too much out of place , and about showOpenMultipleDialog there need to be a cast in place which throws an exception so I have kept the `showOpenDialog` till now. Will be working on classPathGenerator tomorrow.
@oowekyala

This comment has been minimized.

Member

oowekyala commented May 30, 2018

@akshatbahety

about the UI I think this shouldn't be scalable as the components would be just too much out of place

I beg to differ. This screenshot speaks for itself:

capture du 2018-05-30 23-22-30

Non-scalable UIs are infuriating. On the other hand, a good scalable UI expands what the user would like to expand, such that components won't end up "out of place". You should use anchorpane constraints wisely, so that the ListView expands when the window expands.

about showOpenMultipleDialog there need to be a cast in place which throws an exception

I'm guessing you've tried to write File f = (File) chooser.showOpenMultipleDialog(...). Of course this is going to throw an exception, since it the method returns a List. The problem is not the method, but your variable's type. The two methods have a different return type.

public void showAuxPathWizard() throws Exception {
FXMLLoader fxmlLoader = new FXMLLoader(DesignerUtil.getFxml("auxclasspath-setup-popup.fxml.fxml"));

This comment has been minimized.

@oowekyala

oowekyala May 30, 2018

Member

You have two .fxml

Parent root1 = fxmlLoader.load();
Stage stage = new Stage();
stage.initModality(Modality.WINDOW_MODAL);

This comment has been minimized.

@oowekyala

oowekyala May 30, 2018

Member

You have to initialise the owner of the stage too

for (int i = 0; i < list.size(); i++) {
fileList.refresh();
fileList.getItems().add(new File(String.valueOf(list.get(i))));

This comment has been minimized.

@oowekyala

oowekyala May 30, 2018

Member

You're basically readding all the items that are already there. Did you try your code?

This comment has been minimized.

@akshatbahety

akshatbahety May 31, 2018

Contributor

Yes I did and yeah I am trying to figure out to stop those multiple entry display.

This comment has been minimized.

@jsotuyod

jsotuyod May 31, 2018

Member

So, each time you select a file, you add it to the list (line 67) and call this method. Here you go over the whole list and add each entry back to fileList. In other words:

  1. first time, list is empty, single entry is added, you come here, and get 1 item in the list view
  2. second time, list has the previous element, and a second one is aded. You come here, and add again items 1 and 2 (1 gets repeated!)

If you follow Clément's advice, and drop the list, you can remove this method completely. Just add the file to fileList.getItems() in line 67 (instead of the separate list) and at most you will have to add a call to fileList.refresh(); (not sure it's actually needed, but easy to test).

This comment has been minimized.

@oowekyala

oowekyala May 31, 2018

Member

Yes the refresh call is not needed. The ListView (and other controls like TreeView and TableView) stores its items in an ObservableList, which means adding, removing, and reordering elements trigger events which you can subscribe to. The ListView subscribes to these and refreshes its UI whenever a change is notified.

public class AuxClassPathController implements Initializable, SettingsOwner {
private final DesignerRoot designerRoot;
private List<File> list = new ArrayList<>();

This comment has been minimized.

@oowekyala

oowekyala May 30, 2018

Member

You don't actually need a second list. Just use fileListView.getItems()

@FXML
private Button removeFiles;
@FXML
private Button selectFile;

This comment has been minimized.

@oowekyala

oowekyala May 30, 2018

Member

I already explained the naming convention for fx ids...

new FileChooser.ExtensionFilter("Java class files", "*.class")
);
File file = chooser.showOpenDialog((designerRoot.getMainStage()));
list.add(file);

This comment has been minimized.

@oowekyala

oowekyala May 30, 2018

Member

Use fileList.getItems().add(file). The listview knows when you add or remove an item from its items, so you don't need to refresh it manually

}
private void fileSelected() {

This comment has been minimized.

@oowekyala

oowekyala May 30, 2018

Member

Usually the naming convention for methods that are executed on a specific event (handlers) is eg onSelectFileClicked, or onRemoveFileClicked. This makes it evident when the methods are executed.

akshatbahety added some commits May 31, 2018

UI Upgrades (understood that we need the components to be scalable wi…
…ll push that in next commit , and rename the fx:id too ) , dropped the list.
List can be reordered , I'll update the UI by day's end.
I need to still test the classPathLoader @jsotuyod some guidance needed here.
List can be reordered , I'll update the UI by day's end.
I need to still test the classPathLoader @jsotuyod some guidance needed here.
@oowekyala

Did you notice that when closing the popup after adding some jars, when you reopen the popup everything is gone?

The problem here is that the AuxClassPathController isn't supposed to manage the ClassLoader or the classpath by itself. It's supposed to be a read/write view of some property of the application, namely the auxclasspath. That is, when you open the popup, it should be initialised with the previous value of that property, and when you close it, the property should be set on the application.

When I say application here, I mean that this property should be managed by some relevant internal division of the application. Since this data is to be used during the parsing stage, which is managed by the SourceEditorController, I think that that controller is the best candidate.

To sum up the required data flow:

  • The SourceEditorController should maintains two new properties:
    • an ObservableList<File> auxclasspathFiles. This will contain the up-to-date Files that the auxclasspath references
    • a Var<ClassLoader> auxclasspathClassLoader. This will contain the up-to-date ClassLoader that is used for parsing. This property is purely internal, so private
    • auxclasspathClassLoader is bound to auxclasspathFiles, with a mapping in between. Some extra content validation should make sure every file exist, and the classLoader is never null. This can be four lines of ReactFX
  • When the user clicks on the menu item, it's the MainDesignerController that catches the event (like currently). It should ask the SourceEditorController to open the edition popup (calls a public no-arg method on the controller)
  • When the SourceEditorController catches that message (body of the method), it creates an AuxclasspathController and hands it the current value of auxclasspathFiles to initialise the popup.
  • The user edits the files in the ListView. Each change modifies the internal list of the ListView so that they're not directly propagated to the SourceEditorController (in case the user wants to cancel his edits, he has a buffer).
  • If the user cancels their edits, then the popup closes without doing anything else. Nothing has changed in the core of the app, the controller dies quietly.
  • If the user validates their edits, the popup closes, but as it closes it hands the value of the ListView's internal list back to the SourceEditorController. The latter updates its internal auxclasspathFiles property, if it has really changed. auxclasspathClassLoader is updated automatically by the binding

This data flow has the advantage that the app doesn't gain a new core component. SourceEditorController is just made marginally more powerful. Moreover, the auxclasspath is made specific to the editor, instead of global to the whole app. One point I kept in mind while designing the app is that we may want to manage several code snippets/ files in different tabs someday. If we decide to do that, multiple SourceEditorControllers will be used concurrently, each one possibly using a different auxclasspath. That's why SourceEditorController should encapsulate the classloader.

This is not all. With what I described, the app maintains a list of Files, but they're not used anywhere yet.

What remains to do:

  • When SourceEditorController calls astManager::updateCompilationUnit, it should pass the ClassLoader as parameter. ASTManager should use this parameter on the relevant lines (indicated in the file).

http://github.com/pmd/pmd/blob/master/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/SourceEditorController.java#L117

  • SourceEditorController should expose a getter and setter for auxclasspathFiles, with the getter annotated with @PersistentProperty. This will persist the auxclasspath between runs.

Some more improvements are in my comments

<ListView fx:id="fileListView" VBox.vgrow="ALWAYS"/>
</children>
</VBox>
<VBox alignment="CENTER" SplitPane.resizableWithParent="false">

This comment has been minimized.

@oowekyala

oowekyala Jun 3, 2018

Member

The buttons on this UI are a bit overwhelming. Using icons would be better but I can do that change myself since it's not very interesting. What you should do is use content validation to enable the buttons only when they actually do something useful. The rest of the time they should be greyed out and inactive.

You can use a ValidationSupport for that. There's an example for the property edition popup. See here we register some validators:

http://github.com/pmd/pmd/blob/master/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/EditPropertyDialogController.java#L137-L157

And here we disable the commit button whenever preconditions are not fulfilled

http://github.com/pmd/pmd/blob/master/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/EditPropertyDialogController.java#L89

More documentation:

https://controlsfx.bitbucket.io/index.html?org/controlsfx/validation/ValidationSupport.html

This comment has been minimized.

@oowekyala

oowekyala Jun 3, 2018

Member

Also, there should be a cancel button available.

String classPath = "";
for (File f : fileListView.getItems()) {
classPath = classPath + ";" + f.getAbsolutePath();

This comment has been minimized.

@oowekyala

oowekyala Jun 3, 2018

Member

This is not portable, instead of ";" you should use File.pathSeparator. The separator char on Unix systems is :

This comment has been minimized.

@jsotuyod

jsotuyod Jun 4, 2018

Member

since this module uses Java 8, we can actually use streams here rather than looping over the list and creating new string instance all over:

fileListView.getItems().stream().collect(Collectors.joining(File.pathSeparator))

@jsotuyod

Very nice progress, we are almost there

);
List<File> file = chooser.showOpenMultipleDialog(designerRoot.getMainStage());
for (File f : file) {
fileListView.getItems().add(f);

This comment has been minimized.

@jsotuyod

jsotuyod Jun 4, 2018

Member

you can probably just do fileListView.getItems().addAll(file) rather than loop over the list.

Also, since you have a list of files, you probably want to rename the list to files rather than the singular file

String classPath = "";
for (File f : fileListView.getItems()) {
classPath = classPath + ";" + f.getAbsolutePath();

This comment has been minimized.

@jsotuyod

jsotuyod Jun 4, 2018

Member

since this module uses Java 8, we can actually use streams here rather than looping over the list and creating new string instance all over:

fileListView.getItems().stream().collect(Collectors.joining(File.pathSeparator))

public class AuxClassPathController implements Initializable, SettingsOwner {
private final DesignerRoot designerRoot;
private ClassLoader classLoader = getClass().getClassLoader();

This comment has been minimized.

@jsotuyod

jsotuyod Jun 4, 2018

Member

you will probably want to move this away from the UI and into a model object to keep it easily accessible at any time + decouple it from the UI

This comment has been minimized.

@oowekyala

oowekyala Jun 6, 2018

Member

Yes you should remove it.

added the Cancel button
the list needs to be updated in the popup to make the data flow as required
working on validation and left out a couple of changes due to some reasons
@oowekyala

This is not ready to be merged. In its current state the popup is not connected to the rest of the app and does nothing.

@PersistentProperty
public ObservableList<File> getAuxclasspathFiles() {

This comment has been minimized.

@oowekyala

oowekyala Jun 6, 2018

Member

The persistence framework throws an exception because you should return a string here, because it can't convert a list that easily. Look at this for example:

@PersistentProperty
public String getRecentFiles() {
StringBuilder sb = new StringBuilder();
for (File f : recentFiles) {
sb.append(',').append(f.getAbsolutePath());
}
return sb.length() > 0 ? sb.substring(1) : "";
}
public void setRecentFiles(String files) {
List<String> fileNames = Arrays.asList(files.split(","));
Collections.reverse(fileNames);
for (String fileName : fileNames) {
File f = new File(fileName);
recentFiles.push(f);
}
}

public class AuxClassPathController implements Initializable, SettingsOwner {
private final DesignerRoot designerRoot;
private ClassLoader classLoader = getClass().getClassLoader();

This comment has been minimized.

@oowekyala

oowekyala Jun 6, 2018

Member

Yes you should remove it.

private final DesignerRoot designerRoot;
private ClassLoader classLoader = getClass().getClassLoader();
private ValidationSupport validationSupport = new ValidationSupport();

This comment has been minimized.

@oowekyala

oowekyala Jun 6, 2018

Member

You need to add validators.

}
private String classPathGenerator() throws IOException {

This comment has been minimized.

@oowekyala

oowekyala Jun 6, 2018

Member

This shouldn't be here. You should use a similar routine to map the auxclasspathFiles to the auxclasspathClassLoader in SourceEditorController

}
public void setClassPath(String classPath) throws IOException {

This comment has been minimized.

@oowekyala

oowekyala Jun 6, 2018

Member

This should be removed

this.designerRoot = designerRoot;
if (auxClassPathFiles != null) {
fileListView.setItems(auxClassPathFiles);

This comment has been minimized.

@oowekyala

oowekyala Jun 6, 2018

Member

You can't use FXML injected fields in the contructor. Here, the ListView is not yet initialised. You have to put that in the initialise method

}
try {
showAuxPathWizard();

This comment has been minimized.

@oowekyala

oowekyala Jun 6, 2018

Member

Don't put that in the constructor. Let clients call the method themselves.

@@ -62,6 +64,9 @@
private ASTTreeItem selectedTreeItem;
private static final Duration AST_REFRESH_DELAY = Duration.ofMillis(100);
public static ObservableList<File> auxclasspathFiles;

This comment has been minimized.

@oowekyala

oowekyala Jun 6, 2018

Member

I think it would be easier to have here a Var<List<File>>. Btw you need to initialise it, eg Var.newSimpleVar(Collections.emptyList()).

@@ -62,6 +64,9 @@
private ASTTreeItem selectedTreeItem;
private static final Duration AST_REFRESH_DELAY = Duration.ofMillis(100);
public static ObservableList<File> auxclasspathFiles;
private Var<ClassLoader> auxclasspathClassLoader;

This comment has been minimized.

@oowekyala

oowekyala Jun 6, 2018

Member

You need to initialise that as well

Data flow is working properly thanks to @oowekyala for helping me out.
UI is upgraded to make sure buttons are active only when needed (will make a small addition to moveitem up and moveitem down button as discussed)
About the ClassLoader I am a bit confused about how to use `ClasspathClassLoader` I think will be able to merge it today before evening IST.
for (File f : auxclasspathFiles.getValue()) {
sb.append(';').append(f.getAbsolutePath());
}
return sb.length() > 0 ? sb.substring(1) : "";

This comment has been minimized.

@jsotuyod

jsotuyod Jun 7, 2018

Member

if you use streams to set the value, I'd expect to use stream here, for consistency

This comment has been minimized.

@jsotuyod

jsotuyod Jun 7, 2018

Member

avoid the extra empty lines

public void setAuxClassPathFiles(String files) {
List<File> newVal = Arrays.asList(files.split(";")).stream().map(File::new).collect(Collectors.toList());
auxclasspathFiles.setValue(newVal);
}

This comment has been minimized.

@jsotuyod

jsotuyod Jun 7, 2018

Member

you should here initialize a ClasspathClassLoader for auxclasspathClassLoader and have the ASTManager refresh everything using that classloader.

StringBuilder sb = new StringBuilder();
for (File f : auxclasspathFiles.getValue()) {
sb.append(';').append(f.getAbsolutePath());

This comment has been minimized.

@jsotuyod

jsotuyod Jun 7, 2018

Member

any reason to use ";" instead of File.pathSeparator??

public void setAuxClassPathFiles(String files) {
List<File> newVal = Arrays.asList(files.split(";")).stream().map(File::new).collect(Collectors.toList());

This comment has been minimized.

@jsotuyod

jsotuyod Jun 7, 2018

Member

any reason to use ";" instead of File.pathSeparator??

@oowekyala

This comment has been minimized.

Member

oowekyala commented Jun 7, 2018

@akshatbahety ClasspathClassloader is pretty explicit. From what you can see in its constructor, we parse a string representing the classpath into an array of files:

public ClasspathClassLoader(String classpath, ClassLoader parent) throws IOException {
super(initURLs(classpath), parent);
}

But since we already have a collection of files, we don't need to convert to string and back again. So basically you can just create a constructor taking a list of files, convert them to URLs to call the super constructor, then map your Var by doing something like

  private final Val<ClassLoader> auxclasspathClassLoader
                      = auxclasspathFiles.map(files -> new ClasspathClassLoader(files, SourceEditorController.class.getClassLoader()));
@oowekyala

This finally looks feature-complete. Good job!

}
return sb.length() > 0 ? sb.substring(1) : "";
String files = auxclasspathFiles.getValue().stream().map(p -> File.pathSeparator + p.getAbsolutePath())
.collect(Collectors.joining(""));

This comment has been minimized.

@oowekyala

oowekyala Jun 7, 2018

Member

You could use Collectors.joining(File.pathSeparator)

@@ -84,7 +85,7 @@ public Node getCompilationUnit() {
*
* @throws ParseAbortedException if parsing fails and cannot recover
*/
public Node updateCompilationUnit(String source) throws ParseAbortedException {
public Node updateCompilationUnit(String source, Val<ClassLoader> classLoader) throws ParseAbortedException {

This comment has been minimized.

@oowekyala

oowekyala Jun 7, 2018

Member

Pass the classloader directly.

List<URL> urlList = new ArrayList<>();
for (File f : files) {
addFileURLs(urlList, f.toURI().toURL());

This comment has been minimized.

@oowekyala

oowekyala Jun 7, 2018

Member

Don't use addFileUrls, just add to your urlList

private Var<List<File>> auxclasspathFiles = Var.newSimpleVar(Collections.emptyList());
private final Val<ClassLoader> auxclasspathClassLoader = auxclasspathFiles.map(files -> {
try {
new ClasspathClassLoader(files, SourceEditorController.class.getClassLoader());

This comment has been minimized.

@oowekyala

oowekyala Jun 7, 2018

Member

You don't return the classloader here... it just gets lost

}
public void setAuxClassPathFiles(String files) {

This comment has been minimized.

@oowekyala

oowekyala Jun 7, 2018

Member

there's a typo here. The name of the method should be exactly setAuxclasspathFiles (notice the capitalisation) otherwise the persistence framework is displeased ^^

@jsotuyod

This comment has been minimized.

Member

jsotuyod commented Jun 7, 2018

@akshatbahety awesome!

Only 2 things missing:

  1. the build is failing. PMD dogfooding. You shouldn't call .toArray(new ArrayType[arraySize]) but use a 0-size array instead: .toArray(new ArrayType[0])
  2. when the ClasspathClassLoader changes, the ASTManager's refresh AST method should be called.

akshatbahety and others added some commits Jun 8, 2018

@oowekyala

This comment has been minimized.

Member

oowekyala commented Jun 8, 2018

@akshatbahety I fixed a bug and think it's ready to be merged. I'll prettify the popup before doing so

Good job!

@oowekyala oowekyala merged commit e533c54 into pmd:master Jun 8, 2018

1 check passed

continuous-integration/travis-ci/pr The Travis CI build passed
Details

oowekyala added a commit that referenced this pull request Jun 8, 2018

@jsotuyod jsotuyod referenced this pull request Jun 11, 2018

Open

[ui] New rule designer pending issues #714

11 of 21 tasks complete

@oowekyala oowekyala added this to Done in Rule Designer Jun 11, 2018

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment