Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 141 additions & 8 deletions cli/src/main/java/ca/weblite/jdeploy/gui/tabs/CliCommandsPanel.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ public class CliCommandsPanel extends JPanel {
private JButton removeButton;
private JTextField nameField;
private JTextField descriptionField;
private JCheckBox updaterCheckbox;
private JCheckBox launcherCheckbox;
private JCheckBox serviceControllerCheckbox;
private JTextArea argsField;
private JLabel validationLabel;
private ActionListener changeListener;
Expand Down Expand Up @@ -179,6 +182,56 @@ private JPanel createRightPanel() {

formPanel.add(Box.createVerticalStrut(10));

// Implements field (checkboxes)
JPanel implLabelPanel = new JPanel();
implLabelPanel.setOpaque(false);
implLabelPanel.setLayout(new BoxLayout(implLabelPanel, BoxLayout.X_AXIS));
JLabel implLabel = new JLabel("Implements");
implLabel.setFont(implLabel.getFont().deriveFont(Font.BOLD));
implLabelPanel.add(implLabel);
implLabelPanel.add(Box.createHorizontalStrut(5));
implLabelPanel.add(createInfoIcon("<html>Special behaviors for this command.<br>See individual checkbox tooltips for details.</html>"));
implLabelPanel.add(Box.createHorizontalGlue());
implLabelPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
implLabelPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, implLabel.getPreferredSize().height));
formPanel.add(implLabelPanel);

// Checkboxes panel
JPanel checkboxPanel = new JPanel();
checkboxPanel.setOpaque(false);
checkboxPanel.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0));
checkboxPanel.setAlignmentX(Component.LEFT_ALIGNMENT);

updaterCheckbox = new JCheckBox("Updater");
updaterCheckbox.setOpaque(false);
updaterCheckbox.setToolTipText("<html>Intercepts 'update' argument to trigger app updates.<br>" +
"Example: <code>myapp-cli update</code> → calls launcher with --jdeploy:update</html>");
updaterCheckbox.addActionListener(e -> onFieldChanged());
checkboxPanel.add(updaterCheckbox);
checkboxPanel.add(Box.createHorizontalStrut(15));

launcherCheckbox = new JCheckBox("Launcher");
launcherCheckbox.setOpaque(false);
launcherCheckbox.setToolTipText("<html>Launches the desktop GUI application.<br>" +
"Arguments are passed as file paths or URLs to open.<br>" +
"macOS: Uses 'open -a MyApp.app', others call binary directly.</html>");
launcherCheckbox.addActionListener(e -> onFieldChanged());
checkboxPanel.add(launcherCheckbox);
checkboxPanel.add(Box.createHorizontalStrut(15));

serviceControllerCheckbox = new JCheckBox("Service Controller");
serviceControllerCheckbox.setOpaque(false);
serviceControllerCheckbox.setToolTipText("<html>Intercepts 'service' as first argument for daemon control.<br>" +
"Example: <code>myappctl service start</code> → calls launcher with --jdeploy:service start</html>");
serviceControllerCheckbox.addActionListener(e -> onFieldChanged());
checkboxPanel.add(serviceControllerCheckbox);

// Constrain the height of the checkbox panel to prevent vertical expansion
checkboxPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, updaterCheckbox.getPreferredSize().height));
formPanel.add(checkboxPanel);

formPanel.add(Box.createVerticalStrut(10));

// Arguments field
JPanel argsLabelPanel = new JPanel();
argsLabelPanel.setOpaque(false);
Expand Down Expand Up @@ -312,6 +365,9 @@ public void load(JSONObject jdeploy) {
commandsModel.clear();
nameField.setText("");
descriptionField.setText("");
updaterCheckbox.setSelected(false);
launcherCheckbox.setSelected(false);
serviceControllerCheckbox.setSelected(false);
argsField.setForeground(Color.GRAY);
argsField.setText(ARGS_PLACEHOLDER);
validationLabel.setText(" ");
Expand Down Expand Up @@ -434,6 +490,9 @@ private void onCommandSelected(ListSelectionEvent evt) {
if (index < 0) {
nameField.setText("");
descriptionField.setText("");
updaterCheckbox.setSelected(false);
launcherCheckbox.setSelected(false);
serviceControllerCheckbox.setSelected(false);
argsField.setForeground(Color.GRAY);
argsField.setText(ARGS_PLACEHOLDER);
validationLabel.setText(" ");
Expand All @@ -453,12 +512,31 @@ private void loadCommandForEditing(String commandName) {
isUpdatingUI = true;
try {
nameField.setText(commandName);

// Load description and args from the backing data model
JSONObject spec = commandsModel.get(commandName);
if (spec != null) {
descriptionField.setText(spec.optString("description", ""));


// Load implements array
updaterCheckbox.setSelected(false);
launcherCheckbox.setSelected(false);
serviceControllerCheckbox.setSelected(false);

if (spec.has("implements")) {
JSONArray implArray = spec.getJSONArray("implements");
for (int i = 0; i < implArray.length(); i++) {
String impl = implArray.getString(i);
if (CommandSpecParser.IMPL_UPDATER.equals(impl)) {
updaterCheckbox.setSelected(true);
} else if (CommandSpecParser.IMPL_LAUNCHER.equals(impl)) {
launcherCheckbox.setSelected(true);
} else if (CommandSpecParser.IMPL_SERVICE_CONTROLLER.equals(impl)) {
serviceControllerCheckbox.setSelected(true);
}
}
}

// Load args - join array elements with newlines
if (spec.has("args")) {
JSONArray argsArray = spec.getJSONArray("args");
Expand All @@ -475,10 +553,13 @@ private void loadCommandForEditing(String commandName) {
}
} else {
descriptionField.setText("");
updaterCheckbox.setSelected(false);
launcherCheckbox.setSelected(false);
serviceControllerCheckbox.setSelected(false);
argsField.setForeground(Color.GRAY);
argsField.setText(ARGS_PLACEHOLDER);
}

validationLabel.setText(" ");
} finally {
isUpdatingUI = false;
Expand Down Expand Up @@ -537,15 +618,32 @@ private void onNameChanged() {
if (spec == null) {
spec = new JSONObject();
}

// Save current form values into the spec before moving it
String desc = descriptionField.getText().trim();
if (!desc.isEmpty()) {
spec.put("description", desc);
} else {
spec.remove("description");
}


// Save implements array
JSONArray implArray = new JSONArray();
if (updaterCheckbox.isSelected()) {
implArray.put(CommandSpecParser.IMPL_UPDATER);
}
if (launcherCheckbox.isSelected()) {
implArray.put(CommandSpecParser.IMPL_LAUNCHER);
}
if (serviceControllerCheckbox.isSelected()) {
implArray.put(CommandSpecParser.IMPL_SERVICE_CONTROLLER);
}
if (implArray.length() > 0) {
spec.put("implements", implArray);
} else {
spec.remove("implements");
}

String argsText = argsField.getText().trim();
boolean isPlaceholder = argsText.equals(ARGS_PLACEHOLDER.trim()) && argsField.getForeground().equals(Color.GRAY);

Expand All @@ -566,7 +664,7 @@ private void onNameChanged() {
} else {
spec.remove("args");
}

commandsModel.put(name, spec);

// Update the list (this will not trigger selection change)
Expand Down Expand Up @@ -605,6 +703,9 @@ private void removeSelectedCommand() {
removeButton.setEnabled(false);
nameField.setText("");
descriptionField.setText("");
updaterCheckbox.setSelected(false);
launcherCheckbox.setSelected(false);
serviceControllerCheckbox.setSelected(false);
argsField.setForeground(Color.GRAY);
argsField.setText(ARGS_PLACEHOLDER);
validationLabel.setText(" ");
Expand All @@ -631,6 +732,21 @@ private JSONObject buildCommandSpec(String name) {
spec.put("description", desc);
}

// Build implements array
JSONArray implArray = new JSONArray();
if (updaterCheckbox.isSelected()) {
implArray.put(CommandSpecParser.IMPL_UPDATER);
}
if (launcherCheckbox.isSelected()) {
implArray.put(CommandSpecParser.IMPL_LAUNCHER);
}
if (serviceControllerCheckbox.isSelected()) {
implArray.put(CommandSpecParser.IMPL_SERVICE_CONTROLLER);
}
if (implArray.length() > 0) {
spec.put("implements", implArray);
}

String argsText = argsField.getText().trim();
// Don't save placeholder text as actual args
boolean isPlaceholder = argsText.equals(ARGS_PLACEHOLDER.trim()) && argsField.getForeground().equals(Color.GRAY);
Expand Down Expand Up @@ -670,15 +786,32 @@ private void onFieldChanged() {
spec = new JSONObject();
commandsModel.put(currentName, spec);
}

// Update description
String desc = descriptionField.getText().trim();
if (!desc.isEmpty()) {
spec.put("description", desc);
} else {
spec.remove("description");
}


// Update implements array
JSONArray implArray = new JSONArray();
if (updaterCheckbox.isSelected()) {
implArray.put(CommandSpecParser.IMPL_UPDATER);
}
if (launcherCheckbox.isSelected()) {
implArray.put(CommandSpecParser.IMPL_LAUNCHER);
}
if (serviceControllerCheckbox.isSelected()) {
implArray.put(CommandSpecParser.IMPL_SERVICE_CONTROLLER);
}
if (implArray.length() > 0) {
spec.put("implements", implArray);
} else {
spec.remove("implements");
}

// Update args
String argsText = argsField.getText().trim();
boolean isPlaceholder = argsText.equals(ARGS_PLACEHOLDER.trim()) && argsField.getForeground().equals(Color.GRAY);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,4 +183,78 @@ public void testEditDescriptionAndSwitchCommand() {
assertEquals(1, cmd2Args.length());
assertEquals("arg2", cmd2Args.getString(0));
}

@Test
public void testImplementsPropertySaveAndLoad() {
// Create a command with implements array
JSONObject jdeploy = new JSONObject();
JSONObject commands = new JSONObject();

JSONObject cmd1 = new JSONObject();
cmd1.put("description", "Updater command");
JSONArray impl1 = new JSONArray();
impl1.put("updater");
cmd1.put("implements", impl1);

JSONObject cmd2 = new JSONObject();
cmd2.put("description", "Launcher command");
JSONArray impl2 = new JSONArray();
impl2.put("launcher");
cmd2.put("implements", impl2);

JSONObject cmd3 = new JSONObject();
cmd3.put("description", "Service controller with updater");
JSONArray impl3 = new JSONArray();
impl3.put("service_controller");
impl3.put("updater");
cmd3.put("implements", impl3);

commands.put("updater-cmd", cmd1);
commands.put("launcher-cmd", cmd2);
commands.put("service-cmd", cmd3);
jdeploy.put("commands", commands);

// Load and verify
panel.load(jdeploy);

// Save and verify implements are preserved
JSONObject saved = new JSONObject();
panel.save(saved);

JSONObject savedCommands = saved.getJSONObject("commands");
assertEquals(3, savedCommands.length());

// Verify updater-cmd has updater implementation
assertTrue(savedCommands.has("updater-cmd"));
JSONObject savedCmd1 = savedCommands.getJSONObject("updater-cmd");
assertTrue(savedCmd1.has("implements"));
JSONArray savedImpl1 = savedCmd1.getJSONArray("implements");
assertEquals(1, savedImpl1.length());
assertEquals("updater", savedImpl1.getString(0));

// Verify launcher-cmd has launcher implementation
assertTrue(savedCommands.has("launcher-cmd"));
JSONObject savedCmd2 = savedCommands.getJSONObject("launcher-cmd");
assertTrue(savedCmd2.has("implements"));
JSONArray savedImpl2 = savedCmd2.getJSONArray("implements");
assertEquals(1, savedImpl2.length());
assertEquals("launcher", savedImpl2.getString(0));

// Verify service-cmd has both implementations
assertTrue(savedCommands.has("service-cmd"));
JSONObject savedCmd3 = savedCommands.getJSONObject("service-cmd");
assertTrue(savedCmd3.has("implements"));
JSONArray savedImpl3 = savedCmd3.getJSONArray("implements");
assertEquals(2, savedImpl3.length());
// Note: Order might vary, so check both are present
boolean hasServiceController = false;
boolean hasUpdater = false;
for (int i = 0; i < savedImpl3.length(); i++) {
String impl = savedImpl3.getString(i);
if ("service_controller".equals(impl)) hasServiceController = true;
if ("updater".equals(impl)) hasUpdater = true;
}
assertTrue(hasServiceController, "Should have service_controller implementation");
assertTrue(hasUpdater, "Should have updater implementation");
}
}
Loading