This is an automated email from the ASF dual-hosted git repository.
bamaer pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git
The following commit(s) were added to refs/heads/main by this push:
new 6ce473b82c Issue #8564 : Do not warn when closing an unchanged
JavaScript dialog (#8633)
6ce473b82c is described below
commit 6ce473b82c9ea5842b555641e100fd49db2bbf57
Author: Matt Casters <[email protected]>
AuthorDate: Sun Sep 27 11:03:51 2026 +0200
Issue #8564 : Do not warn when closing an unchanged JavaScript dialog
(#8633)
---
.../transforms/javascript/ScriptValuesDialog.java | 9 +-
.../javascript/ScriptValuesDialogChangedTest.java | 219 +++++++++++++++++++++
.../apache/hop/ui/hopgui/ContentEditorWidget.java | 15 +-
3 files changed, 234 insertions(+), 9 deletions(-)
diff --git
a/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesDialog.java
b/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesDialog.java
index fcb74655cb..4fb01bdbde 100644
---
a/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesDialog.java
+++
b/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesDialog.java
@@ -221,7 +221,8 @@ public class ScriptValuesDialog extends BaseTransformDialog
{
.cancel(e -> cancel())
.build();
- lsMod = e -> input.setChanged();
+ // Keep the listener from createShell(). It ignores events while loading,
so filling the
+ // widgets does not mark the transform changed. Replacing it did, and
Cancel then warned.
changed = input.hasChanged();
Control lastControl = wSpacer;
@@ -590,10 +591,12 @@ public class ScriptValuesDialog extends
BaseTransformDialog {
IContentEditorWidget editor =
ContentEditorFacade.createContentEditor(item.getParent(),
"javascript");
+ // setText() notifies modify listeners on the next event-loop turn, which
is after open()
+ // has restored the original changed flag. Loading a tab then looked like
an edit.
if ((strScript != null) && !strScript.isEmpty()) {
- editor.setText(strScript);
+ editor.setTextSuppressModify(strScript);
} else {
- editor.setText(
+ editor.setTextSuppressModify(
BaseMessages.getString(PKG, "ScriptValuesDialogMod.ScriptHere.Label")
+ Const.CR
+ Const.CR);
diff --git
a/plugins/transforms/javascript/src/test/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesDialogChangedTest.java
b/plugins/transforms/javascript/src/test/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesDialogChangedTest.java
new file mode 100644
index 0000000000..74f41a3fc7
--- /dev/null
+++
b/plugins/transforms/javascript/src/test/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesDialogChangedTest.java
@@ -0,0 +1,219 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.pipeline.transforms.javascript;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
+import org.apache.hop.core.Const;
+import org.apache.hop.core.plugins.PluginRegistry;
+import org.apache.hop.core.plugins.TransformPluginType;
+import org.apache.hop.core.variables.Variables;
+import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.pipeline.transform.TransformMeta;
+import org.apache.hop.ui.core.widget.editor.IContentEditorWidget;
+import org.apache.hop.ui.testing.SwtBotTestBase;
+import org.eclipse.swt.custom.CTabFolder;
+import org.eclipse.swt.custom.CTabItem;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swtbot.swt.finder.SWTBot;
+import org.eclipse.swtbot.swt.finder.utils.SWTBotPreferences;
+import org.eclipse.swtbot.swt.finder.waits.DefaultCondition;
+import org.eclipse.swtbot.swt.finder.widgets.SWTBotShell;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Opening the JavaScript dialog and leaving it must not look like an edit.
+ *
+ * <p>The script tab uses the shared content editor. Its {@code setText}
notifies modify listeners
+ * on a later event-loop turn, after the dialog has restored the transform's
original changed flag,
+ * so Cancel warned that the content had changed (issue #8564).
+ *
+ * <p>Tagged {@code uitest} so it is skipped when there is no display. The
default reactor run still
+ * includes it on a desktop; wrap Maven with {@code
tools/with-isolated-display.sh} so the dialog
+ * does not steal focus.
+ */
+@Tag("uitest")
+class ScriptValuesDialogChangedTest extends SwtBotTestBase {
+
+ /**
+ * The script tab builds a syntax-highlighted editor, which loads its
grammar before the dialog
+ * opens. That takes longer than SWTBot's 5 second default on a cold run.
+ */
+ private static final long DIALOG_TIMEOUT_MS = 20_000L;
+
+ private static long defaultTimeout;
+
+ private static final String TRANSFORM_NAME = "javascript";
+
+ private static final String DIALOG_TITLE =
+ BaseMessages.getString(ScriptValuesMeta.class,
"ScriptValuesDialogMod.Shell.Title");
+
+ private static final String WARNING_TITLE =
+ BaseMessages.getString(
+ ScriptValuesMeta.class,
"ScriptValuesModDialog.WarningDialogChanged.Title");
+
+ @BeforeAll
+ static void slowDownSwtBot() {
+ defaultTimeout = SWTBotPreferences.TIMEOUT;
+ SWTBotPreferences.TIMEOUT = DIALOG_TIMEOUT_MS;
+ }
+
+ @AfterAll
+ static void restoreSwtBotTimeout() {
+ SWTBotPreferences.TIMEOUT = defaultTimeout;
+ }
+
+ @Test
+ void openingAndCancellingLeavesTheTransformUnchanged() {
+ ScriptValuesMeta meta = new ScriptValuesMeta();
+ meta.setChanged(false);
+ String script = meta.getJsScripts().getFirst().getScript();
+ String optimizationLevel = meta.getOptimizationLevel();
+ String languageVersion = meta.getLanguageVersion();
+
+ withDialog(
+ openerFor(meta),
+ bot -> {
+ SWTBot dialog = openedDialog(bot);
+ // Read the flag on the UI thread, after any modify notification
queued while the dialog
+ // was built. syncExec runs behind those notifications.
+ display.syncExec(() -> {});
+ assertFalse(meta.hasChanged(), "opening the dialog must not mark the
transform changed");
+ dialog.button(buttonLabel("System.Button.Cancel")).click();
+ });
+
+ assertFalse(meta.hasChanged(), "Cancel must restore the original changed
flag");
+ assertEquals(script, meta.getJsScripts().getFirst().getScript());
+ assertEquals(optimizationLevel, meta.getOptimizationLevel());
+ assertEquals(languageVersion, meta.getLanguageVersion());
+ assertNoWarningLeftOpen();
+ }
+
+ @Test
+ void editingTheScriptIsKeptOnOk() {
+ ScriptValuesMeta meta = new ScriptValuesMeta();
+ meta.setChanged(false);
+ String original = meta.getJsScripts().getFirst().getScript();
+ String edited = original + "var kept = 1;" + Const.CR;
+
+ withDialog(
+ openerFor(meta),
+ bot -> {
+ SWTBot dialog = openedDialog(bot);
+ IContentEditorWidget editor =
scriptEditor(bot.shell(DIALOG_TITLE).widget);
+ // setText updates the document immediately and notifies listeners
on the next turn.
+ display.syncExec(() -> editor.setText(edited));
+ display.syncExec(() -> {});
+ assertTrue(meta.hasChanged(), "editing the script must mark the
transform changed");
+ dialog.button(buttonLabel("System.Button.OK")).click();
+ });
+
+ assertTrue(meta.hasChanged());
+ assertEquals(edited, meta.getJsScripts().getFirst().getScript());
+ assertNoWarningLeftOpen();
+ }
+
+ /**
+ * The shell exists as soon as it is created, and it is not the active shell
until it is opened.
+ * Wait until it is visible, then drive that shell.
+ */
+ private SWTBot openedDialog(SWTBot bot) {
+ SWTBotShell shell = bot.shell(DIALOG_TITLE);
+ bot.waitUntil(
+ new DefaultCondition() {
+ @Override
+ public boolean test() {
+ return shell.isOpen();
+ }
+
+ @Override
+ public String getFailureMessage() {
+ return "JavaScript dialog did not open";
+ }
+ });
+ return shell.bot();
+ }
+
+ private void assertNoWarningLeftOpen() {
+ display.syncExec(
+ () -> {
+ for (Shell openShell : display.getShells()) {
+ if (!openShell.isDisposed()) {
+ assertFalse(
+ WARNING_TITLE.equals(openShell.getText()),
+ "the unsaved-changes warning should not be left open");
+ }
+ }
+ });
+ }
+
+ /** The script tab stores its editor on the selected item, which SWTBot does
not walk. */
+ private static IContentEditorWidget scriptEditor(Shell shell) {
+ AtomicReference<IContentEditorWidget> editor = new AtomicReference<>();
+ display.syncExec(
+ () -> {
+ CTabFolder folder = findTabFolder(shell);
+ assertNotNull(folder, "the dialog should show the script tabs");
+ CTabItem item = folder.getSelection();
+ assertNotNull(item, "a script tab should be selected");
+ editor.set((IContentEditorWidget)
item.getData("ContentEditorWidget"));
+ });
+ assertNotNull(editor.get(), "the selected script tab should hold its
editor");
+ return editor.get();
+ }
+
+ private static CTabFolder findTabFolder(Control control) {
+ if (control instanceof CTabFolder folder) {
+ return folder;
+ }
+ if (control instanceof Composite composite) {
+ for (Control child : composite.getChildren()) {
+ CTabFolder found = findTabFolder(child);
+ if (found != null) {
+ return found;
+ }
+ }
+ }
+ return null;
+ }
+
+ private Consumer<Shell> openerFor(ScriptValuesMeta meta) {
+ PipelineMeta pipelineMeta = pipelineWith(meta);
+ return parent -> new ScriptValuesDialog(parent, new Variables(), meta,
pipelineMeta).open();
+ }
+
+ private static PipelineMeta pipelineWith(ScriptValuesMeta meta) {
+ String pluginId =
PluginRegistry.getInstance().getPluginId(TransformPluginType.class, meta);
+ assertNotNull(pluginId, "JavaScript transform must be registered via
HopEnvironment.init()");
+ PipelineMeta pipelineMeta = new PipelineMeta();
+ pipelineMeta.addTransform(new TransformMeta(pluginId, TRANSFORM_NAME,
meta));
+ meta.setChanged(false);
+ return pipelineMeta;
+ }
+}
diff --git
a/rcp/src/main/java/org/apache/hop/ui/hopgui/ContentEditorWidget.java
b/rcp/src/main/java/org/apache/hop/ui/hopgui/ContentEditorWidget.java
index 5ccf4cd6e7..f1fcbbf515 100644
--- a/rcp/src/main/java/org/apache/hop/ui/hopgui/ContentEditorWidget.java
+++ b/rcp/src/main/java/org/apache/hop/ui/hopgui/ContentEditorWidget.java
@@ -300,13 +300,16 @@ public class ContentEditorWidget implements
IContentEditorWidget {
ContentEditorActions.ID_TOOLBAR_FIND_REPLACE,
sourceViewer.isEditable());
}
- // Update the HopGui main menu items...
+ // Update the HopGui main menu items. There is no main menu when the
editor is built outside a
+ // running Hop GUI, so skip it the same way as a missing toolbar.
GuiMenuWidgets mainMenuWidgets = HopGui.getInstance().getMainMenuWidgets();
- mainMenuWidgets.enableMenuItem(HopGui.ID_MAIN_MENU_EDIT_UNDO, canUndo);
- mainMenuWidgets.enableMenuItem(HopGui.ID_MAIN_MENU_EDIT_REDO, canRedo);
- mainMenuWidgets.enableMenuItem(HopGui.ID_MAIN_MENU_EDIT_CUT, canCut);
- mainMenuWidgets.enableMenuItem(HopGui.ID_MAIN_MENU_EDIT_COPY, canCopy);
- mainMenuWidgets.enableMenuItem(HopGui.ID_MAIN_MENU_EDIT_PASTE, canPaste);
+ if (mainMenuWidgets != null) {
+ mainMenuWidgets.enableMenuItem(HopGui.ID_MAIN_MENU_EDIT_UNDO, canUndo);
+ mainMenuWidgets.enableMenuItem(HopGui.ID_MAIN_MENU_EDIT_REDO, canRedo);
+ mainMenuWidgets.enableMenuItem(HopGui.ID_MAIN_MENU_EDIT_CUT, canCut);
+ mainMenuWidgets.enableMenuItem(HopGui.ID_MAIN_MENU_EDIT_COPY, canCopy);
+ mainMenuWidgets.enableMenuItem(HopGui.ID_MAIN_MENU_EDIT_PASTE, canPaste);
+ }
}
private void addToolbar() {