This is an automated email from the ASF dual-hosted git repository.
hansva 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 7b97a601fc issue #3526 : add Favorites category for transforms and
actions (#7598)
7b97a601fc is described below
commit 7b97a601fc3772e13d02bfee6d0d1e3c9373cc5b
Author: Matt Casters <[email protected]>
AuthorDate: Wed Jul 22 16:46:03 2026 +0200
issue #3526 : add Favorites category for transforms and actions (#7598)
---
.../apache/hop/ui/core/dialog/ContextDialog.java | 171 +++++++++++-----
.../hop/ui/core/gui/ShortcutDisplayUtil.java | 8 +
.../hop/ui/hopgui/context/GuiActionFavorites.java | 223 +++++++++++++++++++++
.../hop/ui/hopgui/context/GuiContextUtil.java | 7 +-
.../pipeline/context/HopGuiPipelineContext.java | 20 +-
.../workflow/context/HopGuiWorkflowContext.java | 24 ++-
.../core/dialog/messages/messages_en_US.properties | 3 +
.../ui/hopgui/context/GuiActionFavoritesTest.java | 161 +++++++++++++++
8 files changed, 552 insertions(+), 65 deletions(-)
diff --git a/ui/src/main/java/org/apache/hop/ui/core/dialog/ContextDialog.java
b/ui/src/main/java/org/apache/hop/ui/core/dialog/ContextDialog.java
index 7dcac7a119..2a17bd2b13 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/dialog/ContextDialog.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/dialog/ContextDialog.java
@@ -24,6 +24,7 @@ import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.function.Supplier;
import org.apache.commons.lang3.StringUtils;
import org.apache.hop.core.Const;
import org.apache.hop.core.Props;
@@ -49,6 +50,7 @@ import org.apache.hop.ui.core.gui.IToolbarContainer;
import org.apache.hop.ui.core.gui.WindowProperty;
import org.apache.hop.ui.core.widget.OsHelper;
import org.apache.hop.ui.hopgui.ToolbarFacade;
+import org.apache.hop.ui.hopgui.context.GuiActionFavorites;
import org.apache.hop.ui.pipeline.transform.BaseTransformDialog;
import org.apache.hop.ui.util.EnvironmentUtils;
import org.eclipse.swt.SWT;
@@ -97,7 +99,8 @@ public class ContextDialog extends Dialog {
public static final String AUDIT_NAME_CATEGORY_STATES = "CategoryStates";
private final Point location;
- private final List<GuiAction> actions;
+ private List<GuiAction> actions;
+ private final Supplier<List<GuiAction>> actionsSupplier;
private final PropsUi props;
private Shell shell;
private Text wSearch;
@@ -303,11 +306,22 @@ public class ContextDialog extends Dialog {
public ContextDialog(
Shell parent, String title, Point location, List<GuiAction> actions,
String contextId) {
+ this(parent, title, location, actions, contextId, null);
+ }
+
+ public ContextDialog(
+ Shell parent,
+ String title,
+ Point location,
+ List<GuiAction> actions,
+ String contextId,
+ Supplier<List<GuiAction>> actionsSupplier) {
super(parent);
this.setText(title);
this.location = location;
this.actions = actions;
+ this.actionsSupplier = actionsSupplier;
props = PropsUi.getInstance();
@@ -328,58 +342,10 @@ public class ContextDialog extends Dialog {
shell.setImage(GuiResource.getInstance().getImageHop());
shell.setLayout(new FormLayout());
- Display display = shell.getDisplay();
-
xMargin = 3 * margin;
yMargin = 2 * margin;
- // Let's take a look at the list of actions and see if we've got
categories to use...
- //
- categories = new ArrayList<>();
- for (GuiAction action : actions) {
- CategoryAndOrder categoryAndOrder;
- if (StringUtils.isNotEmpty(action.getCategory())) {
- categoryAndOrder =
- new CategoryAndOrder(
- action.getCategory(), Const.NVL(action.getCategoryOrder(),
"0"), false);
- } else {
- // Add an "Other" category
- categoryAndOrder = new CategoryAndOrder(CATEGORY_OTHER, "9999", false);
- }
- if (!categories.contains(categoryAndOrder)) {
- categories.add(categoryAndOrder);
- }
- }
-
- categories.sort(Comparator.comparing(o -> o.order));
-
- // Correct the icon size which is multiplied in GuiResource...
- //
- int correctedIconSize = (int) (iconSize / props.getZoomFactor());
-
- // Load the action images
- //
- items.clear();
- for (GuiAction action : actions) {
- ClassLoader classLoader = action.getClassLoader();
- if (classLoader == null) {
- classLoader = ClassLoader.getSystemClassLoader();
- }
- // Load or get from the image cache...
- //
- Image image;
- try {
- image =
- GuiResource.getInstance()
- .getImage(action.getImage(), classLoader, correctedIconSize,
correctedIconSize);
- } catch (Exception e) {
- image =
- GuiResource.getInstance()
- .getSwtImageMissing()
- .getAsBitmapForSize(display, correctedIconSize,
correctedIconSize);
- }
- items.add(new Item(action, image));
- }
+ rebuildCategoriesAndItems();
// Add a search bar at the top...
//
@@ -578,6 +544,7 @@ public class ContextDialog extends Dialog {
// Wait until the dialog is closed
//
+ Display display = shell.getDisplay();
while (!shell.isDisposed()) {
if (!display.readAndDispatch()) {
display.sleep();
@@ -797,6 +764,23 @@ public class ContextDialog extends Dialog {
//
Item item = (Item) areaOwner.getOwner();
if (item != null) {
+ // ALT-Click: toggle transform/action favorite without closing the
dialog (issue #3526)
+ //
+ boolean altClicked = (event.stateMask & SWT.ALT) != 0;
+ if (altClicked &&
GuiActionFavorites.tryToggleFromAction(item.getAction())) {
+ try {
+ HopConfig.getInstance().saveToFile();
+ } catch (Exception e) {
+ new ErrorDialog(
+ shell,
+ BaseMessages.getString(PKG,
"ContextDialog.SaveConfig.Error.Dialog.Header"),
+ BaseMessages.getString(PKG,
"ContextDialog.SaveConfig.Error.Dialog.Message"),
+ e);
+ }
+ refreshActionsFromSupplier();
+ return;
+ }
+
selectedAction = item.getAction();
shiftClicked = (event.stateMask & SWT.SHIFT) != 0;
@@ -812,6 +796,93 @@ public class ContextDialog extends Dialog {
}
}
+ /**
+ * Rebuild the category list and icon items from the current {@link
#actions} list. Preserves
+ * collapsed state of categories when refreshing after a favorites toggle.
+ */
+ private void rebuildCategoriesAndItems() {
+ Map<String, Boolean> previousCollapsed = new HashMap<>();
+ if (categories != null) {
+ for (CategoryAndOrder category : categories) {
+ previousCollapsed.put(category.getCategory(), category.isCollapsed());
+ }
+ }
+
+ categories = new ArrayList<>();
+ for (GuiAction action : actions) {
+ CategoryAndOrder categoryAndOrder;
+ if (StringUtils.isNotEmpty(action.getCategory())) {
+ categoryAndOrder =
+ new CategoryAndOrder(
+ action.getCategory(), Const.NVL(action.getCategoryOrder(),
"0"), false);
+ } else {
+ categoryAndOrder = new CategoryAndOrder(CATEGORY_OTHER, "9999", false);
+ }
+ if (!categories.contains(categoryAndOrder)) {
+ Boolean wasCollapsed =
previousCollapsed.get(categoryAndOrder.getCategory());
+ if (wasCollapsed != null) {
+ categoryAndOrder.setCollapsed(wasCollapsed);
+ }
+ categories.add(categoryAndOrder);
+ }
+ }
+
+ categories.sort(Comparator.comparing(o -> o.order));
+
+ int correctedIconSize = (int) (iconSize / props.getZoomFactor());
+ Display display = shell != null && !shell.isDisposed() ?
shell.getDisplay() : null;
+
+ items.clear();
+ for (GuiAction action : actions) {
+ ClassLoader classLoader = action.getClassLoader();
+ if (classLoader == null) {
+ classLoader = ClassLoader.getSystemClassLoader();
+ }
+ Image image;
+ try {
+ image =
+ GuiResource.getInstance()
+ .getImage(action.getImage(), classLoader, correctedIconSize,
correctedIconSize);
+ } catch (Exception e) {
+ if (display != null) {
+ image =
+ GuiResource.getInstance()
+ .getSwtImageMissing()
+ .getAsBitmapForSize(display, correctedIconSize,
correctedIconSize);
+ } else {
+ image = null;
+ }
+ }
+ items.add(new Item(action, image));
+ }
+ }
+
+ /**
+ * Reload actions from the supplier (after a favorite toggle) and re-apply
the current search
+ * filter so the Favorites category appears/disappears without closing the
dialog.
+ */
+ private void refreshActionsFromSupplier() {
+ if (actionsSupplier == null) {
+ return;
+ }
+ String selectedName = selectedItem != null ?
selectedItem.getAction().getName() : null;
+ actions = actionsSupplier.get();
+ rebuildCategoriesAndItems();
+ String searchText = wSearch != null && !wSearch.isDisposed() ?
wSearch.getText() : "";
+ filter(searchText);
+
+ // Try to re-select the same tool by name after the list refresh
+ //
+ if (selectedName != null) {
+ for (Item item : filteredItems) {
+ if (selectedName.equals(item.getAction().getName())) {
+ selectItem(item, false);
+ break;
+ }
+ }
+ }
+ }
+
private void onResize(Event event) {
updateVerticalBar();
}
diff --git
a/ui/src/main/java/org/apache/hop/ui/core/gui/ShortcutDisplayUtil.java
b/ui/src/main/java/org/apache/hop/ui/core/gui/ShortcutDisplayUtil.java
index 9fcc70bf0b..081f1c59b4 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/gui/ShortcutDisplayUtil.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/gui/ShortcutDisplayUtil.java
@@ -65,6 +65,14 @@ public final class ShortcutDisplayUtil {
return out;
}
+ /**
+ * Returns the display label of the ALT modifier for the current platform:
"⌥" (Option) on macOS,
+ * "ALT" on Windows/Linux. Useful for hints like "ALT-Click to ...".
+ */
+ public static String getAltModifierDisplay() {
+ return Const.isOSX() ? "⌥" : "ALT";
+ }
+
/**
* Converts an SWT key code to the display string used in the configuration
panel (e.g. "⌫" for
* Delete on macOS, "↑" for arrow up).
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/context/GuiActionFavorites.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/context/GuiActionFavorites.java
new file mode 100644
index 0000000000..167b198f6b
--- /dev/null
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/context/GuiActionFavorites.java
@@ -0,0 +1,223 @@
+/*
+ * 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.ui.hopgui.context;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hop.core.Const;
+import org.apache.hop.core.config.HopConfig;
+import org.apache.hop.core.gui.plugin.action.GuiAction;
+import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.ui.core.dialog.ContextDialog;
+import org.apache.hop.ui.core.gui.ShortcutDisplayUtil;
+
+/**
+ * Manages user favorites for transforms (pipeline canvas) and workflow
actions. Favorites are
+ * stored in hop-config.json under guiProperties and shown as a "Favorites"
category in the context
+ * action dialog, immediately below the Basic category.
+ */
+public final class GuiActionFavorites {
+
+ private static final Class<?> PKG = ContextDialog.class; // i18n
+
+ public static final String CONFIG_KEY_TRANSFORMS = "FavoriteTransforms";
+ public static final String CONFIG_KEY_WORKFLOW_ACTIONS =
"FavoriteWorkflowActions";
+
+ /** Category order: after Basic ("1"), before other contextual categories
("2", …). */
+ public static final String CATEGORY_ORDER = "1.5";
+
+ public static final String ID_PREFIX_TRANSFORM =
"pipeline-graph-create-transform-";
+ public static final String ID_PREFIX_TRANSFORM_FAVORITE =
+ "pipeline-graph-create-transform-favorite-";
+ public static final String ID_PREFIX_ACTION =
"workflow-graph-create-workflow-action-";
+ public static final String ID_PREFIX_ACTION_FAVORITE =
+ "workflow-graph-create-workflow-action-favorite-";
+
+ public enum Kind {
+ TRANSFORM(CONFIG_KEY_TRANSFORMS),
+ WORKFLOW_ACTION(CONFIG_KEY_WORKFLOW_ACTIONS);
+
+ private final String configKey;
+
+ Kind(String configKey) {
+ this.configKey = configKey;
+ }
+
+ public String getConfigKey() {
+ return configKey;
+ }
+ }
+
+ private GuiActionFavorites() {
+ // utility
+ }
+
+ public static String getFavoritesCategoryName() {
+ return BaseMessages.getString(PKG, "ContextDialog.Category.Favorites");
+ }
+
+ /**
+ * Append the ALT-Click hint to a plugin description for use as a GuiAction
tooltip. The modifier
+ * is shown with the platform specific label: "⌥" (Option) on macOS, "ALT"
elsewhere.
+ *
+ * @param description the plugin description (may be null)
+ * @param favorite true if the plugin is already a favorite (show remove
hint)
+ * @return description plus newline and hint
+ */
+ public static String tooltipWithFavoriteHint(String description, boolean
favorite) {
+ String modifier = ShortcutDisplayUtil.getAltModifierDisplay();
+ String hint =
+ favorite
+ ? BaseMessages.getString(PKG, "ContextDialog.Favorite.RemoveHint",
modifier)
+ : BaseMessages.getString(PKG, "ContextDialog.Favorite.AddHint",
modifier);
+ return Const.NVL(description, "") + hint;
+ }
+
+ public static List<String> getFavoriteIds(Kind kind) {
+ return parseIds(HopConfig.getGuiProperty(kind.getConfigKey()));
+ }
+
+ public static boolean isFavorite(Kind kind, String pluginId) {
+ if (StringUtils.isEmpty(pluginId)) {
+ return false;
+ }
+ return getFavoriteIds(kind).contains(pluginId);
+ }
+
+ /**
+ * Toggle the given plugin ID in the favorites list and persist to hop
config.
+ *
+ * @return true if the plugin is a favorite after the toggle, false if
removed
+ */
+ public static boolean toggle(Kind kind, String pluginId) {
+ if (StringUtils.isEmpty(pluginId)) {
+ return false;
+ }
+ Set<String> ids = new LinkedHashSet<>(getFavoriteIds(kind));
+ boolean nowFavorite;
+ if (ids.contains(pluginId)) {
+ ids.remove(pluginId);
+ nowFavorite = false;
+ } else {
+ ids.add(pluginId);
+ nowFavorite = true;
+ }
+ saveIds(kind, new ArrayList<>(ids));
+ return nowFavorite;
+ }
+
+ /**
+ * If the action id refers to a create-transform or create-action entry,
toggle its favorite
+ * status.
+ *
+ * @param action the selected gui action
+ * @return true if a favorite was toggled (caller should refresh the dialog)
+ */
+ public static boolean tryToggleFromAction(GuiAction action) {
+ if (action == null || StringUtils.isEmpty(action.getId())) {
+ return false;
+ }
+ String id = action.getId();
+ KindAndPluginId resolved = resolve(id);
+ if (resolved == null) {
+ return false;
+ }
+ toggle(resolved.kind, resolved.pluginId);
+ return true;
+ }
+
+ /**
+ * Create a Favorites-category copy of a create action for the given plugin
id.
+ *
+ * @param base the original create action
+ * @param kind transform or workflow action
+ * @param pluginId plugin id
+ * @return a new GuiAction with Favorites category and order
+ */
+ public static GuiAction createFavoriteAction(GuiAction base, Kind kind,
String pluginId) {
+ GuiAction favorite = new GuiAction(base);
+ favorite.setId(favoriteId(kind, pluginId));
+ favorite.setCategory(getFavoritesCategoryName());
+ favorite.setCategoryOrder(CATEGORY_ORDER);
+ return favorite;
+ }
+
+ public static String favoriteId(Kind kind, String pluginId) {
+ return switch (kind) {
+ case TRANSFORM -> ID_PREFIX_TRANSFORM_FAVORITE + pluginId;
+ case WORKFLOW_ACTION -> ID_PREFIX_ACTION_FAVORITE + pluginId;
+ };
+ }
+
+ public static String createId(Kind kind, String pluginId) {
+ return switch (kind) {
+ case TRANSFORM -> ID_PREFIX_TRANSFORM + pluginId;
+ case WORKFLOW_ACTION -> ID_PREFIX_ACTION + pluginId;
+ };
+ }
+
+ static List<String> parseIds(String csv) {
+ List<String> result = new ArrayList<>();
+ if (StringUtils.isEmpty(csv)) {
+ return result;
+ }
+ Arrays.stream(csv.split(","))
+ .map(String::trim)
+ .filter(StringUtils::isNotEmpty)
+ .forEach(result::add);
+ return result;
+ }
+
+ static String serializeIds(List<String> ids) {
+ if (ids == null || ids.isEmpty()) {
+ return "";
+ }
+ return String.join(",", ids);
+ }
+
+ private static void saveIds(Kind kind, List<String> ids) {
+ // Persist the in-memory guiProperties value; the context dialog saves
hop-config.json after a
+ // successful toggle so unit tests can exercise this without writing the
config file.
+ HopConfig.setGuiProperty(kind.getConfigKey(), serializeIds(ids));
+ }
+
+ private static KindAndPluginId resolve(String actionId) {
+ if (actionId.startsWith(ID_PREFIX_TRANSFORM_FAVORITE)) {
+ return new KindAndPluginId(
+ Kind.TRANSFORM,
actionId.substring(ID_PREFIX_TRANSFORM_FAVORITE.length()));
+ }
+ if (actionId.startsWith(ID_PREFIX_TRANSFORM)) {
+ return new KindAndPluginId(Kind.TRANSFORM,
actionId.substring(ID_PREFIX_TRANSFORM.length()));
+ }
+ if (actionId.startsWith(ID_PREFIX_ACTION_FAVORITE)) {
+ return new KindAndPluginId(
+ Kind.WORKFLOW_ACTION,
actionId.substring(ID_PREFIX_ACTION_FAVORITE.length()));
+ }
+ if (actionId.startsWith(ID_PREFIX_ACTION)) {
+ return new KindAndPluginId(
+ Kind.WORKFLOW_ACTION, actionId.substring(ID_PREFIX_ACTION.length()));
+ }
+ return null;
+ }
+
+ private record KindAndPluginId(Kind kind, String pluginId) {}
+}
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/context/GuiContextUtil.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/context/GuiContextUtil.java
index 7f49d04120..35cabcd6b0 100644
--- a/ui/src/main/java/org/apache/hop/ui/hopgui/context/GuiContextUtil.java
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/context/GuiContextUtil.java
@@ -162,7 +162,12 @@ public class GuiContextUtil {
contextDialog =
new ContextDialog(
- parent, message, clickLocation, actions,
contextHandler.getContextId());
+ parent,
+ message,
+ clickLocation,
+ actions,
+ contextHandler.getContextId(),
+ contextHandler::getSupportedActions);
shellDialogMap.put(parent.getText(), contextDialog);
GuiAction selectedAction = contextDialog.open();
shellDialogMap.remove(parent.getText());
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/context/HopGuiPipelineContext.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/context/HopGuiPipelineContext.java
index 8587dc92a7..ea5092673b 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/context/HopGuiPipelineContext.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/context/HopGuiPipelineContext.java
@@ -32,6 +32,7 @@ import org.apache.hop.core.plugins.TransformPluginType;
import org.apache.hop.pipeline.PipelineMeta;
import org.apache.hop.ui.hopgui.PaletteEngineFilter;
import org.apache.hop.ui.hopgui.context.BaseGuiContextHandler;
+import org.apache.hop.ui.hopgui.context.GuiActionFavorites;
import org.apache.hop.ui.hopgui.context.IGuiContextHandler;
import org.apache.hop.ui.hopgui.file.pipeline.HopGuiPipelineGraph;
@@ -86,17 +87,20 @@ public class HopGuiPipelineContext extends
BaseGuiContextHandler implements IGui
if (!filter.isPluginAllowed(transformPlugin)) {
continue;
}
+ String pluginId = transformPlugin.getIds()[0];
+ boolean favorite =
GuiActionFavorites.isFavorite(GuiActionFavorites.Kind.TRANSFORM, pluginId);
GuiAction createTransformAction =
new GuiAction(
- "pipeline-graph-create-transform-" + transformPlugin.getIds()[0],
+ GuiActionFavorites.createId(GuiActionFavorites.Kind.TRANSFORM,
pluginId),
GuiActionType.Create,
transformPlugin.getName(),
- transformPlugin.getDescription(),
+ GuiActionFavorites.tooltipWithFavoriteHint(
+ transformPlugin.getDescription(), favorite),
transformPlugin.getImageFile(),
(shiftClicked, controlClicked, t) ->
pipelineGraph.pipelineTransformDelegate.newTransform(
pipelineMeta,
- transformPlugin.getIds()[0],
+ pluginId,
transformPlugin.getName(),
transformPlugin.getDescription(),
controlClicked,
@@ -113,11 +117,17 @@ public class HopGuiPipelineContext extends
BaseGuiContextHandler implements IGui
try {
createTransformAction.setClassLoader(registry.getClassLoader(transformPlugin));
} catch (HopPluginException e) {
- LogChannel.UI.logError(
- "Unable to get classloader for transform plugin " +
transformPlugin.getIds()[0], e);
+ LogChannel.UI.logError("Unable to get classloader for transform plugin
" + pluginId, e);
}
createTransformAction.getKeywords().add(transformPlugin.getCategory());
actions.add(createTransformAction);
+
+ // Duplicate under Favorites when the user marked this transform as
favorite (issue #3526)
+ if (favorite) {
+ actions.add(
+ GuiActionFavorites.createFavoriteAction(
+ createTransformAction, GuiActionFavorites.Kind.TRANSFORM,
pluginId));
+ }
}
return actions;
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/context/HopGuiWorkflowContext.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/context/HopGuiWorkflowContext.java
index 042eea05a5..13930d1ec1 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/context/HopGuiWorkflowContext.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/context/HopGuiWorkflowContext.java
@@ -31,6 +31,7 @@ import org.apache.hop.core.plugins.IPlugin;
import org.apache.hop.core.plugins.PluginRegistry;
import org.apache.hop.ui.hopgui.PaletteEngineFilter;
import org.apache.hop.ui.hopgui.context.BaseGuiContextHandler;
+import org.apache.hop.ui.hopgui.context.GuiActionFavorites;
import org.apache.hop.ui.hopgui.context.IGuiContextHandler;
import org.apache.hop.ui.hopgui.file.workflow.HopGuiWorkflowGraph;
import org.apache.hop.workflow.WorkflowMeta;
@@ -87,20 +88,19 @@ public class HopGuiWorkflowContext extends
BaseGuiContextHandler implements IGui
continue;
}
+ String pluginId = actionPlugin.getIds()[0];
+ boolean favorite =
+
GuiActionFavorites.isFavorite(GuiActionFavorites.Kind.WORKFLOW_ACTION,
pluginId);
GuiAction createActionGuiAction =
new GuiAction(
- "workflow-graph-create-workflow-action-" +
actionPlugin.getIds()[0],
+
GuiActionFavorites.createId(GuiActionFavorites.Kind.WORKFLOW_ACTION, pluginId),
GuiActionType.Create,
actionPlugin.getName(),
- actionPlugin.getDescription(),
+
GuiActionFavorites.tooltipWithFavoriteHint(actionPlugin.getDescription(),
favorite),
actionPlugin.getImageFile(),
(shiftClicked, controlClicked, t) ->
workflowGraph.workflowActionDelegate.newAction(
- workflowMeta,
- actionPlugin.getIds()[0],
- actionPlugin.getName(),
- controlClicked,
- click));
+ workflowMeta, pluginId, actionPlugin.getName(),
controlClicked, click));
createActionGuiAction.getKeywords().addAll(Arrays.asList(actionPlugin.getKeywords()));
// Also search on the English name/category/keywords for non-English
locales (issue #2633)
createActionGuiAction.getKeywords().addAll(Arrays.asList(actionPlugin.getEnglishKeywords()));
@@ -110,11 +110,17 @@ public class HopGuiWorkflowContext extends
BaseGuiContextHandler implements IGui
try {
createActionGuiAction.setClassLoader(registry.getClassLoader(actionPlugin));
} catch (HopPluginException e) {
- LogChannel.UI.logError(
- "Unable to get classloader for action plugin " +
actionPlugin.getIds()[0], e);
+ LogChannel.UI.logError("Unable to get classloader for action plugin "
+ pluginId, e);
}
createActionGuiAction.getKeywords().add(actionPlugin.getCategory());
guiActions.add(createActionGuiAction);
+
+ // Duplicate under Favorites when the user marked this action as
favorite (issue #3526)
+ if (favorite) {
+ guiActions.add(
+ GuiActionFavorites.createFavoriteAction(
+ createActionGuiAction,
GuiActionFavorites.Kind.WORKFLOW_ACTION, pluginId));
+ }
}
return guiActions;
diff --git
a/ui/src/main/resources/org/apache/hop/ui/core/dialog/messages/messages_en_US.properties
b/ui/src/main/resources/org/apache/hop/ui/core/dialog/messages/messages_en_US.properties
index 7e0760d31b..07434d07f9 100644
---
a/ui/src/main/resources/org/apache/hop/ui/core/dialog/messages/messages_en_US.properties
+++
b/ui/src/main/resources/org/apache/hop/ui/core/dialog/messages/messages_en_US.properties
@@ -43,6 +43,9 @@ CheckResultDialog.TextDialog.Title=View message
CheckResultDialog.Title=Results of pipeline checks
CheckResultDialog.TransformName.Label=TransformName
CheckResultDialog.WarningsErrors.Label=Warnings and errors
+ContextDialog.Category.Favorites=Favorites
+ContextDialog.Favorite.AddHint=\n{0}-Click to add to favorite
+ContextDialog.Favorite.RemoveHint=\n{0}-Click to remove from favorite
ContextDialog.GuiAction.CollapseCategories.Tooltip=Collapse all categories
ContextDialog.GuiAction.ExpandCategories.Tooltip=Expand all categories
ContextDialog.GuiAction.FixedWidth.Label=Fixed width
diff --git
a/ui/src/test/java/org/apache/hop/ui/hopgui/context/GuiActionFavoritesTest.java
b/ui/src/test/java/org/apache/hop/ui/hopgui/context/GuiActionFavoritesTest.java
new file mode 100644
index 0000000000..87fd6c5e88
--- /dev/null
+++
b/ui/src/test/java/org/apache/hop/ui/hopgui/context/GuiActionFavoritesTest.java
@@ -0,0 +1,161 @@
+/*
+ * 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.ui.hopgui.context;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Arrays;
+import java.util.List;
+import org.apache.hop.core.config.HopConfig;
+import org.apache.hop.core.gui.plugin.action.GuiAction;
+import org.apache.hop.core.gui.plugin.action.GuiActionType;
+import org.apache.hop.ui.hopgui.context.GuiActionFavorites.Kind;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class GuiActionFavoritesTest {
+
+ private String savedTransforms;
+ private String savedActions;
+
+ @BeforeEach
+ void setUp() {
+ savedTransforms =
HopConfig.getGuiProperty(GuiActionFavorites.CONFIG_KEY_TRANSFORMS);
+ savedActions =
HopConfig.getGuiProperty(GuiActionFavorites.CONFIG_KEY_WORKFLOW_ACTIONS);
+ HopConfig.setGuiProperty(GuiActionFavorites.CONFIG_KEY_TRANSFORMS, "");
+ HopConfig.setGuiProperty(GuiActionFavorites.CONFIG_KEY_WORKFLOW_ACTIONS,
"");
+ }
+
+ @AfterEach
+ void tearDown() {
+ if (savedTransforms == null) {
+
HopConfig.readGuiProperties().remove(GuiActionFavorites.CONFIG_KEY_TRANSFORMS);
+ } else {
+ HopConfig.setGuiProperty(GuiActionFavorites.CONFIG_KEY_TRANSFORMS,
savedTransforms);
+ }
+ if (savedActions == null) {
+
HopConfig.readGuiProperties().remove(GuiActionFavorites.CONFIG_KEY_WORKFLOW_ACTIONS);
+ } else {
+ HopConfig.setGuiProperty(GuiActionFavorites.CONFIG_KEY_WORKFLOW_ACTIONS,
savedActions);
+ }
+ }
+
+ @Test
+ void parseAndSerializeIds() {
+ assertTrue(GuiActionFavorites.parseIds(null).isEmpty());
+ assertTrue(GuiActionFavorites.parseIds("").isEmpty());
+ assertEquals(List.of("a", "b", "c"), GuiActionFavorites.parseIds("a,
b,c"));
+ assertEquals("a,b,c", GuiActionFavorites.serializeIds(Arrays.asList("a",
"b", "c")));
+ assertEquals("", GuiActionFavorites.serializeIds(List.of()));
+ }
+
+ @Test
+ void toggleTransformFavorite() {
+ assertFalse(GuiActionFavorites.isFavorite(Kind.TRANSFORM,
"TextFileInput"));
+
+ assertTrue(GuiActionFavorites.toggle(Kind.TRANSFORM, "TextFileInput"));
+ assertTrue(GuiActionFavorites.isFavorite(Kind.TRANSFORM, "TextFileInput"));
+ assertEquals(List.of("TextFileInput"),
GuiActionFavorites.getFavoriteIds(Kind.TRANSFORM));
+
+ assertFalse(GuiActionFavorites.toggle(Kind.TRANSFORM, "TextFileInput"));
+ assertFalse(GuiActionFavorites.isFavorite(Kind.TRANSFORM,
"TextFileInput"));
+ assertTrue(GuiActionFavorites.getFavoriteIds(Kind.TRANSFORM).isEmpty());
+ }
+
+ @Test
+ void transformAndActionListsAreIndependent() {
+ GuiActionFavorites.toggle(Kind.TRANSFORM, "SelectValues");
+ GuiActionFavorites.toggle(Kind.WORKFLOW_ACTION, "PIPELINE");
+
+ assertTrue(GuiActionFavorites.isFavorite(Kind.TRANSFORM, "SelectValues"));
+ assertFalse(GuiActionFavorites.isFavorite(Kind.TRANSFORM, "PIPELINE"));
+ assertTrue(GuiActionFavorites.isFavorite(Kind.WORKFLOW_ACTION,
"PIPELINE"));
+ assertFalse(GuiActionFavorites.isFavorite(Kind.WORKFLOW_ACTION,
"SelectValues"));
+ }
+
+ @Test
+ void tryToggleFromActionId() {
+ GuiAction create =
+ new GuiAction(
+ GuiActionFavorites.createId(Kind.TRANSFORM, "FilterRows"),
+ GuiActionType.Create,
+ "Filter rows",
+ "desc",
+ "image.svg",
+ (s, c, t) -> {});
+ assertTrue(GuiActionFavorites.tryToggleFromAction(create));
+ assertTrue(GuiActionFavorites.isFavorite(Kind.TRANSFORM, "FilterRows"));
+
+ GuiAction favorite =
+ new GuiAction(
+ GuiActionFavorites.favoriteId(Kind.TRANSFORM, "FilterRows"),
+ GuiActionType.Create,
+ "Filter rows",
+ "desc",
+ "image.svg",
+ (s, c, t) -> {});
+ assertTrue(GuiActionFavorites.tryToggleFromAction(favorite));
+ assertFalse(GuiActionFavorites.isFavorite(Kind.TRANSFORM, "FilterRows"));
+
+ GuiAction other =
+ new GuiAction(
+ "pipeline-graph-edit-properties",
+ GuiActionType.Modify,
+ "Edit",
+ "d",
+ "i",
+ (s, c, t) -> {});
+ assertFalse(GuiActionFavorites.tryToggleFromAction(other));
+ }
+
+ @Test
+ void createFavoriteActionSetsCategoryAndOrder() {
+ GuiAction base =
+ new GuiAction(
+ GuiActionFavorites.createId(Kind.WORKFLOW_ACTION, "START"),
+ GuiActionType.Create,
+ "Start",
+ "desc",
+ "image.svg",
+ (s, c, t) -> {});
+ base.setCategory("General");
+ base.setCategoryOrder("9999_General");
+
+ GuiAction favorite =
+ GuiActionFavorites.createFavoriteAction(base, Kind.WORKFLOW_ACTION,
"START");
+
+ assertEquals(GuiActionFavorites.favoriteId(Kind.WORKFLOW_ACTION, "START"),
favorite.getId());
+ assertEquals(GuiActionFavorites.CATEGORY_ORDER,
favorite.getCategoryOrder());
+ assertEquals(base.getActionLambda(), favorite.getActionLambda());
+ assertEquals("Start", favorite.getName());
+ }
+
+ @Test
+ void multiFavoriteOrderPreserved() {
+ GuiActionFavorites.toggle(Kind.TRANSFORM, "A");
+ GuiActionFavorites.toggle(Kind.TRANSFORM, "B");
+ GuiActionFavorites.toggle(Kind.TRANSFORM, "C");
+ assertEquals(List.of("A", "B", "C"),
GuiActionFavorites.getFavoriteIds(Kind.TRANSFORM));
+
+ GuiActionFavorites.toggle(Kind.TRANSFORM, "B");
+ assertEquals(List.of("A", "C"),
GuiActionFavorites.getFavoriteIds(Kind.TRANSFORM));
+ }
+}