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 abbc57a6aa Issue #7891 : Add named table views to TableView (#7946)
abbc57a6aa is described below
commit abbc57a6aabdc053a89103fb3808a0eb92994074
Author: Matt Casters <[email protected]>
AuthorDate: Sat Aug 15 10:41:23 2026 +0200
Issue #7891 : Add named table views to TableView (#7946)
Allow users to define reusable named column subsets and order on
preview grids and other TableView instances. Views are applied
in-place by hiding unused columns, persisted in the active
AuditManager, and documented in the user manual.
Fixes #7891
---
.../images/hop-gui/pipeline/table-views-dialog.png | Bin 0 -> 420612 bytes
.../pages/hop-gui/perspective-configuration.adoc | 2 +-
.../pages/pipeline/run-preview-debug-pipeline.adoc | 26 +
.../modules/ROOT/pages/protips/index.adoc | 5 +
.../java/org/apache/hop/history/AuditStateMap.java | 13 +
.../org/apache/hop/history/AuditManagerTest.java | 28 +
.../ui/core/dialog/TableViewColumnViewDialog.java | 582 +++++++++++++++++++++
.../org/apache/hop/ui/core/widget/TableView.java | 300 +++++++++++
.../hop/ui/core/widget/TableViewColumnView.java | 41 +-
.../ui/core/widget/TableViewColumnViewManager.java | 149 ++++++
.../hop/ui/core/widget/TableViewColumnViews.java | 63 +++
.../core/dialog/messages/messages_en_US.properties | 25 +
.../core/widget/messages/messages_en_US.properties | 4 +
.../widget/TableViewColumnViewManagerTest.java | 78 +++
.../ui/core/widget/TableViewColumnViewsTest.java | 75 +++
15 files changed, 1361 insertions(+), 30 deletions(-)
diff --git
a/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/pipeline/table-views-dialog.png
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/pipeline/table-views-dialog.png
new file mode 100644
index 0000000000..bc33b81d71
Binary files /dev/null and
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/pipeline/table-views-dialog.png
differ
diff --git
a/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-configuration.adoc
b/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-configuration.adoc
index 263d5a1e4b..2b39b2f5b0 100644
---
a/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-configuration.adoc
+++
b/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-configuration.adoc
@@ -113,7 +113,7 @@ The `Look & Feel` configuration options allow you to
configure various aspects o
|Canvas Grid Size|default grid size to use for workflows and pipelines in the
Hop GUI editor |16
|Show Canvas Grid|show the grid (dotted) for workflows and pipelines in Hop
GUI |no
|Hide the menu bar|Do not show the menu bar. If enabled (default), the menu
options are available from the Apache Hop icon in Hop GUI's upper left corner.
|yes
-|Show a toolbar above tables|Show a toolbar with options to cut/copy/paste,
move lines up/down etc in table views (e.g. in preview dialogs, transform
configuration options)|yes
+|Show a toolbar above tables|Show a toolbar with options to cut/copy/paste,
move lines up/down, navigate to a column, manage
xref:pipeline/run-preview-debug-pipeline.adoc#table-views[table views] etc in
table grids (e.g. in preview dialogs, transform configuration options)|yes
|Dark mode|use dark mode. On Hop Web and Windows you can toggle this; on macOS
and other desktop platforms it follows the operating system theme. Changing it
in Hop Web reloads the page to apply the theme. |N/A
|Preferred Language
a|the default language to use in Hop GUI.
diff --git
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/run-preview-debug-pipeline.adoc
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/run-preview-debug-pipeline.adoc
index b5e979618b..55f91350fc 100644
---
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/run-preview-debug-pipeline.adoc
+++
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/run-preview-debug-pipeline.adoc
@@ -106,3 +106,29 @@ To change the pipeline run configuration, click
**Configure**.
The collected rows are shown in the preview window when execution pauses or
finishes.
image::hop-gui/pipeline/preview-pipeline.png[Preview pipeline,75%,align="left"]
+
+[[table-views]]
+== Table views
+
+Preview windows (and other table grids in Hop Gui) can contain a large number
of columns.
+A **table view** is a named subset of those columns, in the order you want to
see them.
+The underlying rows are not changed: hidden columns stay in the data and are
still exported, copied or saved.
+
+Open the **Table views** dialog from the table toolbar
image:getting-started/icons/view.svg[Table views,25px,align="bottom"] icon,
from the row context menu, or from the column header context menu.
+
+image::hop-gui/pipeline/table-views-dialog.png[Table views dialog on a wide
preview,75%,align="left"]
+
+In the dialog:
+
+* Create, rename or delete a view. **Use current layout** fills the view from
the columns that are visible now.
+* Move fields between **Available columns** and **Columns in view**, and use
**Move up** / **Move down** to set the display order.
+* **Apply** shows only the selected columns, in that order. **Show all
columns** restores the full table.
+* **Save** stores the view so you can reuse it later.
+
+Views are stored per project in the Hop audit folder and are matched by column
name.
+A view such as `customer-bk-fields` can be applied to any table that has those
field names, not only the preview where you created it.
+
+You can also hide a single column from the column header context menu, or drag
headers to reorder them (desktop Hop Gui).
+Those one-off changes can be captured with **Use current layout** and then
saved as a named view.
+
+The table toolbar itself can be turned on or off with **Show a toolbar above
tables** in the xref:hop-gui/perspective-configuration.adoc[Configuration]
perspective.
diff --git a/docs/hop-user-manual/modules/ROOT/pages/protips/index.adoc
b/docs/hop-user-manual/modules/ROOT/pages/protips/index.adoc
index 5a716b90ab..f0d540f4ec 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/protips/index.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/protips/index.adoc
@@ -41,6 +41,11 @@
include::../snippets/variables/hop-windows-shell-encoding.adoc[]
CTRL-K::
In any table view in a Hop dialog, select one or more lines and use `CTRL-K`
to remove all lines but your selection
+Table views::
+On a wide preview (or any other table grid), use the view icon on the table
toolbar to define a named subset and order of columns.
+Views are stored per project and matched by column name, so you can reuse them
the next time you inspect similar data.
+See xref:pipeline/run-preview-debug-pipeline.adoc#table-views[Table views].
+
CTRL-SHIFT-Click::
Hover your mouse pointer over any pipeline action in a workflow, pipeline or
workflow executor transform etc and use `CTRL-SHIFT-Click` to open that item in
a new tab.
The same behavior can be triggered by hovering over an item and hitting the
`Z` key.
diff --git a/engine/src/main/java/org/apache/hop/history/AuditStateMap.java
b/engine/src/main/java/org/apache/hop/history/AuditStateMap.java
index 96ab82171a..46571108e3 100644
--- a/engine/src/main/java/org/apache/hop/history/AuditStateMap.java
+++ b/engine/src/main/java/org/apache/hop/history/AuditStateMap.java
@@ -51,4 +51,17 @@ public class AuditStateMap {
public AuditState get(String name) {
return nameStateMap.get(name);
}
+
+ /**
+ * Remove the audit state stored under {@code name}.
+ *
+ * @param name The name of the object whose state should be removed
+ * @return the previous state, or {@code null} if there was none
+ */
+ public AuditState remove(String name) {
+ if (nameStateMap == null || name == null) {
+ return null;
+ }
+ return nameStateMap.remove(name);
+ }
}
diff --git a/engine/src/test/java/org/apache/hop/history/AuditManagerTest.java
b/engine/src/test/java/org/apache/hop/history/AuditManagerTest.java
index 8fd67ca6f5..ae89eefe44 100644
--- a/engine/src/test/java/org/apache/hop/history/AuditManagerTest.java
+++ b/engine/src/test/java/org/apache/hop/history/AuditManagerTest.java
@@ -18,6 +18,7 @@ package org.apache.hop.history;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
@@ -180,4 +181,31 @@ class AuditManagerTest {
stateMap = AuditManager.getActive().loadAuditStateMap(group, type);
assertTrue(stateMap.getNameStateMap().isEmpty(), "State should be
cleared");
}
+
+ @Test
+ void testRemoveState() throws HopException {
+ String group = "hop-gui";
+ String type = "table-view";
+
+ Map<String, Object> first = new HashMap<>();
+ first.put("columns", List.of("id", "name"));
+ AuditManager.storeState(
+ org.apache.hop.core.logging.LogChannel.GENERAL, group, type, "Customer
keys", first);
+
+ Map<String, Object> second = new HashMap<>();
+ second.put("columns", List.of("order_id"));
+ AuditManager.storeState(
+ org.apache.hop.core.logging.LogChannel.GENERAL, group, type, "Order
dates", second);
+
+ AuditStateMap stateMap = AuditManager.getActive().loadAuditStateMap(group,
type);
+ assertEquals(2, stateMap.getNameStateMap().size(), "Both views should be
stored");
+
+ assertNotNull(stateMap.remove("Customer keys"));
+ AuditManager.getActive().saveAuditStateMap(group, type, stateMap);
+
+ stateMap = AuditManager.getActive().loadAuditStateMap(group, type);
+ assertNull(stateMap.get("Customer keys"), "Removed view should be gone");
+ assertNotNull(stateMap.get("Order dates"), "The other view should remain");
+ assertEquals(1, stateMap.getNameStateMap().size());
+ }
}
diff --git
a/ui/src/main/java/org/apache/hop/ui/core/dialog/TableViewColumnViewDialog.java
b/ui/src/main/java/org/apache/hop/ui/core/dialog/TableViewColumnViewDialog.java
new file mode 100644
index 0000000000..7dc035385f
--- /dev/null
+++
b/ui/src/main/java/org/apache/hop/ui/core/dialog/TableViewColumnViewDialog.java
@@ -0,0 +1,582 @@
+/*
+ * 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.core.dialog;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hop.core.Const;
+import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.ui.core.PropsUi;
+import org.apache.hop.ui.core.gui.GuiResource;
+import org.apache.hop.ui.core.gui.HopNamespace;
+import org.apache.hop.ui.core.gui.WindowProperty;
+import org.apache.hop.ui.core.widget.ColumnInfo;
+import org.apache.hop.ui.core.widget.TableView;
+import org.apache.hop.ui.core.widget.TableViewColumnView;
+import org.apache.hop.ui.core.widget.TableViewColumnViewManager;
+import org.apache.hop.ui.pipeline.transform.BaseTransformDialog;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.SashForm;
+import org.eclipse.swt.layout.FormAttachment;
+import org.eclipse.swt.layout.FormData;
+import org.eclipse.swt.layout.FormLayout;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Dialog;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+
+/** Create, edit, apply and persist named {@link TableView} column views. */
+public class TableViewColumnViewDialog extends Dialog {
+ private static final Class<?> PKG = TableViewColumnViewDialog.class;
+
+ private final PropsUi props;
+ private final TableView tableView;
+ private final String[] tableColumnNames;
+ private final Set<String> tableColumnNameSet;
+ private final String group;
+
+ private final Map<String, TableViewColumnView> views = new LinkedHashMap<>();
+
+ private Shell shell;
+ private org.eclipse.swt.widgets.List wViews;
+ private org.eclipse.swt.widgets.List wAvailable;
+ private org.eclipse.swt.widgets.List wShown;
+ private Label wlStatus;
+
+ public TableViewColumnViewDialog(Shell parent, TableView tableView) {
+ super(parent, SWT.NONE);
+ this.props = PropsUi.getInstance();
+ this.tableView = tableView;
+ this.tableColumnNames = columnNames(tableView);
+ this.tableColumnNameSet = new HashSet<>(Arrays.asList(tableColumnNames));
+ String namespace;
+ try {
+ namespace = HopNamespace.getNamespace();
+ } catch (Exception e) {
+ namespace = "hop-gui";
+ }
+ this.group = namespace;
+ }
+
+ public void open() {
+ Shell parent = getParent();
+ shell = new Shell(parent, SWT.DIALOG_TRIM | SWT.RESIZE | SWT.MAX |
SWT.MIN);
+ PropsUi.setLook(shell);
+ shell.setImage(GuiResource.getInstance().getImageView());
+ shell.setText(BaseMessages.getString(PKG,
"TableViewColumnViewDialog.Title"));
+
+ FormLayout formLayout = new FormLayout();
+ formLayout.marginWidth = PropsUi.getFormMargin();
+ formLayout.marginHeight = PropsUi.getFormMargin();
+ shell.setLayout(formLayout);
+
+ int margin = PropsUi.getMargin();
+
+ Button wApply = new Button(shell, SWT.PUSH);
+ wApply.setText(BaseMessages.getString(PKG,
"TableViewColumnViewDialog.Apply.Label"));
+ wApply.addListener(SWT.Selection, e -> apply());
+
+ Button wShowAll = new Button(shell, SWT.PUSH);
+ wShowAll.setText(BaseMessages.getString(PKG,
"TableViewColumnViewDialog.ShowAll.Label"));
+ wShowAll.addListener(SWT.Selection, e -> showAll());
+
+ Button wSave = new Button(shell, SWT.PUSH);
+ wSave.setText(BaseMessages.getString(PKG, "System.Button.Save"));
+ wSave.addListener(SWT.Selection, e -> save());
+
+ Button wClose = new Button(shell, SWT.PUSH);
+ wClose.setText(BaseMessages.getString(PKG, "System.Button.Close"));
+ wClose.addListener(SWT.Selection, e -> close());
+
+ BaseTransformDialog.positionBottomButtons(
+ shell, new Button[] {wApply, wShowAll, wSave, wClose}, margin, null);
+
+ wlStatus = new Label(shell, SWT.LEFT);
+ PropsUi.setLook(wlStatus);
+ FormData fdStatus = new FormData();
+ fdStatus.left = new FormAttachment(0, 0);
+ fdStatus.right = new FormAttachment(100, 0);
+ fdStatus.bottom = new FormAttachment(wApply, -margin);
+ wlStatus.setLayoutData(fdStatus);
+
+ Label wlViews = new Label(shell, SWT.LEFT);
+ wlViews.setText(BaseMessages.getString(PKG,
"TableViewColumnViewDialog.Views.Label"));
+ PropsUi.setLook(wlViews);
+ FormData fdlViews = new FormData();
+ fdlViews.left = new FormAttachment(0, 0);
+ fdlViews.top = new FormAttachment(0, 0);
+ wlViews.setLayoutData(fdlViews);
+
+ Composite viewButtons = new Composite(shell, SWT.NONE);
+ PropsUi.setLook(viewButtons);
+ GridLayout buttonLayout = new GridLayout(4, false);
+ buttonLayout.marginWidth = 0;
+ buttonLayout.marginHeight = 0;
+ viewButtons.setLayout(buttonLayout);
+ FormData fdViewButtons = new FormData();
+ fdViewButtons.left = new FormAttachment(0, 0);
+ fdViewButtons.top = new FormAttachment(wlViews, margin);
+ fdViewButtons.right = new FormAttachment(100, 0);
+ viewButtons.setLayoutData(fdViewButtons);
+
+ Button wNew = new Button(viewButtons, SWT.PUSH);
+ wNew.setText(BaseMessages.getString(PKG, "System.Button.New"));
+ wNew.addListener(SWT.Selection, e -> newView());
+
+ Button wRename = new Button(viewButtons, SWT.PUSH);
+ wRename.setText(BaseMessages.getString(PKG,
"TableViewColumnViewDialog.Rename.Label"));
+ wRename.addListener(SWT.Selection, e -> renameView());
+
+ Button wDelete = new Button(viewButtons, SWT.PUSH);
+ wDelete.setText(BaseMessages.getString(PKG, "System.Button.Delete"));
+ wDelete.addListener(SWT.Selection, e -> deleteView());
+
+ Button wUseCurrent = new Button(viewButtons, SWT.PUSH);
+ wUseCurrent.setText(BaseMessages.getString(PKG,
"TableViewColumnViewDialog.UseCurrent.Label"));
+ wUseCurrent.addListener(SWT.Selection, e -> useCurrentLayout());
+
+ wViews = new org.eclipse.swt.widgets.List(shell, SWT.BORDER | SWT.SINGLE |
SWT.V_SCROLL);
+ PropsUi.setLook(wViews);
+ FormData fdViews = new FormData();
+ fdViews.left = new FormAttachment(0, 0);
+ fdViews.top = new FormAttachment(viewButtons, margin);
+ fdViews.right = new FormAttachment(100, 0);
+ fdViews.height = (int) (120 * PropsUi.getNativeZoomFactor());
+ wViews.setLayoutData(fdViews);
+ wViews.addListener(SWT.Selection, e -> viewSelected());
+
+ SashForm sash = new SashForm(shell, SWT.HORIZONTAL);
+ FormData fdSash = new FormData();
+ fdSash.left = new FormAttachment(0, 0);
+ fdSash.top = new FormAttachment(wViews, margin);
+ fdSash.right = new FormAttachment(100, 0);
+ fdSash.bottom = new FormAttachment(wlStatus, -margin);
+ sash.setLayoutData(fdSash);
+
+ Composite left =
+ createListPane(
+ sash, BaseMessages.getString(PKG,
"TableViewColumnViewDialog.Available.Label"));
+ wAvailable = (org.eclipse.swt.widgets.List) left.getData("list");
+ wAvailable.addListener(SWT.DefaultSelection, e -> addSelected());
+
+ Composite middle = new Composite(sash, SWT.NONE);
+ PropsUi.setLook(middle);
+ middle.setLayout(new FormLayout());
+
+ Composite gButtons = new Composite(middle, SWT.NONE);
+ GridLayout grid = new GridLayout(1, true);
+ gButtons.setLayout(grid);
+ PropsUi.setLook(gButtons);
+
+ Button wAddOne = createMoveButton(gButtons, " > ", this::addSelected);
+ Button wAddAll = createMoveButton(gButtons, " >> ", this::addAll);
+ Button wRemoveOne = createMoveButton(gButtons, " < ",
this::removeSelected);
+ Button wRemoveAll = createMoveButton(gButtons, " << ", this::removeAll);
+ wAddOne.setToolTipText(BaseMessages.getString(PKG,
"TableViewColumnViewDialog.AddOne.ToolTip"));
+ wAddAll.setToolTipText(BaseMessages.getString(PKG,
"TableViewColumnViewDialog.AddAll.ToolTip"));
+ wRemoveOne.setToolTipText(
+ BaseMessages.getString(PKG,
"TableViewColumnViewDialog.RemoveOne.ToolTip"));
+ wRemoveAll.setToolTipText(
+ BaseMessages.getString(PKG,
"TableViewColumnViewDialog.RemoveAll.ToolTip"));
+
+ FormData fdButtons = new FormData();
+ wAddAll.pack();
+ fdButtons.left = new FormAttachment(50, -(wAddAll.getSize().x / 2) - 5);
+ fdButtons.top = new FormAttachment(20, 0);
+ gButtons.setLayoutData(fdButtons);
+
+ Composite right =
+ createListPane(sash, BaseMessages.getString(PKG,
"TableViewColumnViewDialog.Shown.Label"));
+ wShown = (org.eclipse.swt.widgets.List) right.getData("list");
+ wShown.addListener(SWT.DefaultSelection, e -> removeSelected());
+
+ Composite orderButtons = new Composite(right, SWT.NONE);
+ PropsUi.setLook(orderButtons);
+ GridLayout orderLayout = new GridLayout(2, true);
+ orderLayout.marginWidth = 0;
+ orderLayout.marginHeight = 0;
+ orderButtons.setLayout(orderLayout);
+ FormData fdOrder = new FormData();
+ fdOrder.left = new FormAttachment(0, 0);
+ fdOrder.right = new FormAttachment(100, 0);
+ fdOrder.bottom = new FormAttachment(100, 0);
+ orderButtons.setLayoutData(fdOrder);
+
+ Button wUp = new Button(orderButtons, SWT.PUSH);
+ wUp.setText(BaseMessages.getString(PKG,
"TableViewColumnViewDialog.MoveUp.Label"));
+ wUp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ wUp.addListener(SWT.Selection, e -> moveShown(-1));
+
+ Button wDown = new Button(orderButtons, SWT.PUSH);
+ wDown.setText(BaseMessages.getString(PKG,
"TableViewColumnViewDialog.MoveDown.Label"));
+ wDown.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ wDown.addListener(SWT.Selection, e -> moveShown(1));
+
+ ((FormData) wShown.getLayoutData()).bottom = new
FormAttachment(orderButtons, -margin);
+
+ sash.setWeights(new int[] {42, 16, 42});
+
+ shell.addListener(SWT.Close, e -> close());
+
+ loadViews();
+ refreshColumnLists(java.util.List.of());
+ updateStatus();
+
+ BaseTransformDialog.setSize(shell);
+ shell.open();
+
+ Display display = shell.getDisplay();
+ while (!shell.isDisposed()) {
+ if (!display.readAndDispatch()) {
+ display.sleep();
+ }
+ }
+ }
+
+ private Composite createListPane(SashForm sash, String title) {
+ Composite pane = new Composite(sash, SWT.NONE);
+ PropsUi.setLook(pane);
+ pane.setLayout(new FormLayout());
+
+ Label label = new Label(pane, SWT.LEFT);
+ label.setText(title);
+ PropsUi.setLook(label);
+ FormData fdLabel = new FormData();
+ fdLabel.left = new FormAttachment(0, 0);
+ fdLabel.top = new FormAttachment(0, 0);
+ label.setLayoutData(fdLabel);
+
+ org.eclipse.swt.widgets.List list =
+ new org.eclipse.swt.widgets.List(
+ pane, SWT.BORDER | SWT.MULTI | SWT.V_SCROLL | SWT.H_SCROLL);
+ PropsUi.setLook(list);
+ FormData fdList = new FormData();
+ fdList.left = new FormAttachment(0, 0);
+ fdList.top = new FormAttachment(label, 0);
+ fdList.right = new FormAttachment(100, 0);
+ fdList.bottom = new FormAttachment(100, 0);
+ list.setLayoutData(fdList);
+ pane.setData("list", list);
+ return pane;
+ }
+
+ private Button createMoveButton(Composite parent, String text, Runnable
action) {
+ Button button = new Button(parent, SWT.PUSH);
+ button.setText(text);
+ button.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ button.addListener(SWT.Selection, e -> action.run());
+ return button;
+ }
+
+ private void loadViews() {
+ views.clear();
+ for (TableViewColumnView view : TableViewColumnViewManager.list(group)) {
+ views.put(view.getName(), view);
+ }
+ refreshViewList(null);
+ }
+
+ private void refreshViewList(String selectName) {
+ String[] names = views.keySet().toArray(new String[0]);
+ Arrays.sort(names, String.CASE_INSENSITIVE_ORDER);
+ wViews.setItems(names);
+ if (selectName != null) {
+ int index = Const.indexOfString(selectName, names);
+ if (index >= 0) {
+ wViews.setSelection(index);
+ }
+ }
+ }
+
+ private void viewSelected() {
+ String name = selectedViewName();
+ if (name == null) {
+ return;
+ }
+ TableViewColumnView view = views.get(name);
+ java.util.List<String> shown =
+ view != null && view.getColumnNames() != null
+ ? new ArrayList<>(view.getColumnNames())
+ : new ArrayList<>();
+ refreshColumnLists(shown);
+ updateStatus();
+ }
+
+ private void newView() {
+ String name = promptName("");
+ if (name == null) {
+ return;
+ }
+ if (views.containsKey(name) && !confirmOverwrite(name)) {
+ return;
+ }
+ TableViewColumnView view = new TableViewColumnView(name, new
ArrayList<>());
+ views.put(name, view);
+ refreshViewList(name);
+ refreshColumnLists(java.util.List.of());
+ updateStatus();
+ }
+
+ private void renameView() {
+ String oldName = selectedViewName();
+ if (oldName == null) {
+ return;
+ }
+ String newName = promptName(oldName);
+ if (newName == null || newName.equals(oldName)) {
+ return;
+ }
+ if (views.containsKey(newName) && !confirmOverwrite(newName)) {
+ return;
+ }
+ TableViewColumnView view = views.remove(oldName);
+ if (view == null) {
+ view = new TableViewColumnView();
+ }
+ view.setName(newName);
+ view.setColumnNames(currentShownColumns());
+ views.remove(newName);
+ views.put(newName, view);
+ TableViewColumnViewManager.delete(group, oldName);
+ TableViewColumnViewManager.save(group, view);
+ refreshViewList(newName);
+ }
+
+ private void deleteView() {
+ String name = selectedViewName();
+ if (name == null) {
+ return;
+ }
+ MessageBox box = new MessageBox(shell, SWT.YES | SWT.NO |
SWT.ICON_QUESTION);
+ box.setText(BaseMessages.getString(PKG,
"TableViewColumnViewDialog.Delete.Title"));
+ box.setMessage(BaseMessages.getString(PKG,
"TableViewColumnViewDialog.Delete.Message", name));
+ if ((box.open() & SWT.YES) == 0) {
+ return;
+ }
+ views.remove(name);
+ TableViewColumnViewManager.delete(group, name);
+ refreshViewList(null);
+ refreshColumnLists(java.util.List.of());
+ updateStatus();
+ }
+
+ private void useCurrentLayout() {
+ refreshColumnLists(tableView.getVisibleColumnNamesInOrder());
+ updateStatus();
+ }
+
+ private void addSelected() {
+ moveItems(wAvailable, wShown, wAvailable.getSelection());
+ }
+
+ private void addAll() {
+ moveItems(wAvailable, wShown, wAvailable.getItems());
+ }
+
+ private void removeSelected() {
+ moveItems(wShown, wAvailable, wShown.getSelection());
+ }
+
+ private void removeAll() {
+ moveItems(wShown, wAvailable, wShown.getItems());
+ }
+
+ private void moveItems(
+ org.eclipse.swt.widgets.List from, org.eclipse.swt.widgets.List to,
String[] items) {
+ for (String item : items) {
+ if (from.indexOf(item) >= 0) {
+ from.remove(item);
+ }
+ boolean addToAvailable = to == wAvailable &&
tableColumnNameSet.contains(item);
+ boolean addToShown = to == wShown;
+ if ((addToAvailable || addToShown) && to.indexOf(item) < 0) {
+ to.add(item);
+ }
+ }
+ updateStatus();
+ }
+
+ private void moveShown(int delta) {
+ int[] selection = wShown.getSelectionIndices();
+ if (selection.length != 1) {
+ return;
+ }
+ int from = selection[0];
+ int to = from + delta;
+ if (to < 0 || to >= wShown.getItemCount()) {
+ return;
+ }
+ String[] items = wShown.getItems();
+ String moving = items[from];
+ items[from] = items[to];
+ items[to] = moving;
+ wShown.setItems(items);
+ wShown.setSelection(to);
+ }
+
+ private void apply() {
+ java.util.List<String> shown = currentShownColumns();
+ if (shown.isEmpty()) {
+ MessageBox box = new MessageBox(shell, SWT.OK | SWT.ICON_INFORMATION);
+ box.setText(BaseMessages.getString(PKG,
"TableViewColumnViewDialog.Title"));
+ box.setMessage(BaseMessages.getString(PKG,
"TableViewColumnViewDialog.EmptyView.Message"));
+ box.open();
+ return;
+ }
+ if (!tableView.applyColumnView(shown)) {
+ MessageBox box = new MessageBox(shell, SWT.OK | SWT.ICON_INFORMATION);
+ box.setText(BaseMessages.getString(PKG,
"TableViewColumnViewDialog.Title"));
+ box.setMessage(BaseMessages.getString(PKG,
"TableViewColumnViewDialog.NoMatch.Message"));
+ box.open();
+ }
+ }
+
+ private void showAll() {
+ tableView.resetColumnView();
+ }
+
+ private void save() {
+ String name = selectedViewName();
+ if (name == null) {
+ name = promptName("");
+ if (name == null) {
+ return;
+ }
+ if (views.containsKey(name) && !confirmOverwrite(name)) {
+ return;
+ }
+ }
+ TableViewColumnView view = views.computeIfAbsent(name, key -> new
TableViewColumnView());
+ view.setName(name);
+ view.setColumnNames(currentShownColumns());
+ TableViewColumnViewManager.save(group, view);
+ views.put(name, view);
+ refreshViewList(name);
+ }
+
+ private void close() {
+ props.setScreen(new WindowProperty(shell));
+ shell.dispose();
+ }
+
+ private void refreshColumnLists(java.util.List<String> shownNames) {
+ java.util.List<String> shown = new ArrayList<>();
+ Set<String> shownSet = new HashSet<>();
+ for (String name : shownNames) {
+ if (StringUtils.isEmpty(name) || shownSet.contains(name)) {
+ continue;
+ }
+ shown.add(name);
+ shownSet.add(name);
+ }
+ wShown.setItems(shown.toArray(new String[0]));
+
+ java.util.List<String> available = new ArrayList<>();
+ for (String name : tableColumnNames) {
+ if (!shownSet.contains(name)) {
+ available.add(name);
+ }
+ }
+ wAvailable.setItems(available.toArray(new String[0]));
+ }
+
+ private void updateStatus() {
+ java.util.List<String> shown = currentShownColumns();
+ int matching = 0;
+ int missing = 0;
+ for (String name : shown) {
+ if (tableColumnNameSet.contains(name)) {
+ matching++;
+ } else {
+ missing++;
+ }
+ }
+ String status =
+ BaseMessages.getString(
+ PKG,
+ "TableViewColumnViewDialog.Status.Visible",
+ Integer.toString(matching),
+ Integer.toString(tableColumnNames.length));
+ if (missing > 0) {
+ status +=
+ " "
+ + BaseMessages.getString(
+ PKG, "TableViewColumnViewDialog.Status.Missing",
Integer.toString(missing));
+ }
+ wlStatus.setText(status);
+ }
+
+ private java.util.List<String> currentShownColumns() {
+ return new ArrayList<>(Arrays.asList(wShown.getItems()));
+ }
+
+ private String selectedViewName() {
+ String[] selection = wViews.getSelection();
+ if (selection == null || selection.length == 0) {
+ return null;
+ }
+ return selection[0];
+ }
+
+ private String promptName(String initial) {
+ EnterStringDialog dialog =
+ new EnterStringDialog(
+ shell,
+ Const.NVL(initial, ""),
+ BaseMessages.getString(PKG,
"TableViewColumnViewDialog.Name.Title"),
+ BaseMessages.getString(PKG,
"TableViewColumnViewDialog.Name.Message"));
+ String name = dialog.open();
+ if (name == null) {
+ return null;
+ }
+ name = name.trim();
+ if (name.isEmpty()) {
+ MessageBox box = new MessageBox(shell, SWT.OK | SWT.ICON_INFORMATION);
+ box.setText(BaseMessages.getString(PKG,
"TableViewColumnViewDialog.Title"));
+ box.setMessage(BaseMessages.getString(PKG,
"TableViewColumnViewDialog.Name.Empty"));
+ box.open();
+ return null;
+ }
+ return name;
+ }
+
+ private boolean confirmOverwrite(String name) {
+ MessageBox box = new MessageBox(shell, SWT.YES | SWT.NO |
SWT.ICON_QUESTION);
+ box.setText(BaseMessages.getString(PKG,
"TableViewColumnViewDialog.Overwrite.Title"));
+ box.setMessage(
+ BaseMessages.getString(PKG,
"TableViewColumnViewDialog.Overwrite.Message", name));
+ return (box.open() & SWT.YES) != 0;
+ }
+
+ private static String[] columnNames(TableView tableView) {
+ ColumnInfo[] columns = tableView.getColumns();
+ String[] names = new String[columns.length];
+ for (int i = 0; i < columns.length; i++) {
+ names[i] = Const.NVL(columns[i].getName(), "");
+ }
+ return names;
+ }
+}
diff --git a/ui/src/main/java/org/apache/hop/ui/core/widget/TableView.java
b/ui/src/main/java/org/apache/hop/ui/core/widget/TableView.java
index e12864812b..7bf3d2b308 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/widget/TableView.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/widget/TableView.java
@@ -55,6 +55,7 @@ import org.apache.hop.ui.core.dialog.EnterConditionDialog;
import org.apache.hop.ui.core.dialog.EnterSelectionDialog;
import org.apache.hop.ui.core.dialog.ErrorDialog;
import org.apache.hop.ui.core.dialog.MessageBox;
+import org.apache.hop.ui.core.dialog.TableViewColumnViewDialog;
import org.apache.hop.ui.core.gui.GuiResource;
import org.apache.hop.ui.core.gui.GuiToolbarWidgets;
import org.apache.hop.ui.core.gui.IToolbarContainer;
@@ -151,6 +152,7 @@ public class TableView extends Composite {
"tableview-toolbar-10320-filtered-selection";
public static final String ID_TOOLBAR_NAVIGATE_TO_COLUMN =
"tableview-toolbar-10330-navigate-to-column";
+ public static final String ID_TOOLBAR_TABLE_VIEWS =
"tableview-toolbar-10340-table-views";
public static final String ID_TOOLBAR_COPY_SELECTED =
"tableview-toolbar-10400-copy-selected";
public static final String ID_TOOLBAR_PASTE_TO_TABLE =
"tableview-toolbar-10410-paste-to-table";
public static final String ID_TOOLBAR_CUT_SELECTED =
"tableview-toolbar-10420-cut-selected";
@@ -235,6 +237,8 @@ public class TableView extends Composite {
private boolean addIndexColumn = true;
private final Color nullTextColor;
private final List<String> removeToolItems;
+ private final Set<Integer> hiddenDataColumns = new HashSet<>();
+ private int[] rememberedWidths;
/**
* Create a table to add to a dialog
@@ -577,6 +581,9 @@ public class TableView extends Composite {
tableColumn[i + 1].setAlignment(SWT.RIGHT);
}
tableColumn[i + 1].pack();
+ if (!EnvironmentUtils.getInstance().isWeb()) {
+ tableColumn[i + 1].setMoveable(true);
+ }
}
table.setHeaderVisible(true);
@@ -735,6 +742,7 @@ public class TableView extends Composite {
toolbarWidgets.enableToolbarItem(ID_TOOLBAR_CLEAR_SELECTION, hasRows);
toolbarWidgets.enableToolbarItem(ID_TOOLBAR_FILTERED_SELECTION, hasRows);
toolbarWidgets.enableToolbarItem(ID_TOOLBAR_NAVIGATE_TO_COLUMN,
columns.length > 0);
+ toolbarWidgets.enableToolbarItem(ID_TOOLBAR_TABLE_VIEWS, columns.length >
0);
}
private MouseListener createTableMouseListener() {
@@ -1462,6 +1470,16 @@ public class TableView extends Composite {
OsHelper.customizeMenuitemText(
BaseMessages.getString(PKG, "TableView.menu.NavigateToColumn")));
miNavigateToColumn.addListener(SWT.Selection, e -> navigateToColumn());
+ }
+
+ if (!removeToolItems.contains(ID_TOOLBAR_TABLE_VIEWS)) {
+ MenuItem miTableViews = new MenuItem(mRow, SWT.NONE);
+ miTableViews.setText(
+ OsHelper.customizeMenuitemText(BaseMessages.getString(PKG,
"TableView.menu.TableViews")));
+ miTableViews.setImage(GuiResource.getInstance().getImageView());
+ miTableViews.addListener(SWT.Selection, e -> editTableViews());
+ new MenuItem(mRow, SWT.SEPARATOR);
+ } else if (!removeToolItems.contains(ID_TOOLBAR_NAVIGATE_TO_COLUMN)) {
new MenuItem(mRow, SWT.SEPARATOR);
}
@@ -1536,6 +1554,7 @@ public class TableView extends Composite {
}
table.setMenu(mRow);
+ addHeaderContextMenu(mRow);
}
protected void addToolbar() {
@@ -3539,6 +3558,12 @@ public class TableView extends Composite {
for (int c = 0; c < table.getColumnCount(); c++) {
TableColumn tc = table.getColumn(c);
+ if (c > 0 && hiddenDataColumns.contains(c - 1)) {
+ if (tc.getWidth() != 0) {
+ tc.setWidth(0);
+ }
+ continue;
+ }
int max = 0;
if (header) {
max = TextSizeUtilFacade.textExtent(tc.getText()).x + extraForMargin;
@@ -4230,6 +4255,9 @@ public class TableView extends Composite {
}
// tableColumn[0] is the row-number (#) column; data columns start at
index 1.
+ if (hiddenDataColumns.contains(index)) {
+ showDataColumn(index);
+ }
TableColumn tableCol = tableColumn[index + 1];
if (tableCol != null && !tableCol.isDisposed()) {
table.showColumn(tableCol);
@@ -4237,6 +4265,278 @@ public class TableView extends Composite {
}
}
+ @GuiToolbarElement(
+ root = ID_TOOLBAR,
+ id = ID_TOOLBAR_TABLE_VIEWS,
+ image = "ui/images/view.svg",
+ toolTip = "i18n::TableView.ToolBarWidget.TableViews.ToolTip")
+ public void editTableViews() {
+ if (columns.length == 0) {
+ return;
+ }
+ new TableViewColumnViewDialog(getShell(), this).open();
+ }
+
+ /**
+ * Hide and reorder data columns so that only {@code columnNames} stay
visible, in that order.
+ * Matching is by column name. Cell data is left in place.
+ *
+ * @return {@code true} if at least one column matched
+ */
+ public boolean applyColumnView(List<String> columnNames) {
+ if (table == null || table.isDisposed() || columns.length == 0) {
+ return false;
+ }
+ String[] available = new String[columns.length];
+ for (int i = 0; i < columns.length; i++) {
+ available[i] = Const.NVL(columns[i].getName(), "");
+ }
+ List<Integer> visible =
TableViewColumnViews.resolveColumnIndices(available, columnNames);
+ if (visible.isEmpty()) {
+ return false;
+ }
+ ensureRememberedWidths();
+ hiddenDataColumns.clear();
+ Set<Integer> visibleSet = new HashSet<>(visible);
+ for (int i = 0; i < columns.length; i++) {
+ if (!visibleSet.contains(i)) {
+ hiddenDataColumns.add(i);
+ }
+ }
+ applyHiddenAndOrder(visible);
+ return true;
+ }
+
+ /** Restore every data column to its original order and remembered width. */
+ public void resetColumnView() {
+ if (table == null || table.isDisposed()) {
+ return;
+ }
+ hiddenDataColumns.clear();
+ for (int i = 1; i < tableColumn.length; i++) {
+ TableColumn tc = tableColumn[i];
+ if (tc == null || tc.isDisposed()) {
+ continue;
+ }
+ tc.setResizable(true);
+ int restore = rememberedWidths != null ? rememberedWidths[i] : 0;
+ if (restore > 0) {
+ tc.setWidth(restore);
+ } else if (tc.getWidth() <= 0) {
+ tc.pack();
+ }
+ }
+ setColumnOrderSafe(naturalColumnOrder());
+ }
+
+ /**
+ * Visible data-column names in current visual order (the # column is
omitted). Hidden columns
+ * (width 0) are skipped.
+ */
+ public List<String> getVisibleColumnNamesInOrder() {
+ List<String> names = new ArrayList<>();
+ if (table == null || table.isDisposed()) {
+ return names;
+ }
+ for (int tableIdx : getColumnOrderSafe()) {
+ if (tableIdx <= 0 || tableIdx > columns.length) {
+ continue;
+ }
+ int dataIdx = tableIdx - 1;
+ if (hiddenDataColumns.contains(dataIdx)) {
+ continue;
+ }
+ TableColumn tc = tableColumn[tableIdx];
+ if (tc == null || tc.isDisposed() || tc.getWidth() <= 0) {
+ continue;
+ }
+ names.add(Const.NVL(columns[dataIdx].getName(), ""));
+ }
+ return names;
+ }
+
+ public void hideDataColumn(int dataIndex) {
+ if (dataIndex < 0 || dataIndex >= columns.length) {
+ return;
+ }
+ if (table == null || table.isDisposed()) {
+ return;
+ }
+ ensureRememberedWidths();
+ rememberWidth(dataIndex + 1);
+ hiddenDataColumns.add(dataIndex);
+ TableColumn tc = tableColumn[dataIndex + 1];
+ if (tc != null && !tc.isDisposed()) {
+ tc.setWidth(0);
+ tc.setResizable(false);
+ }
+ }
+
+ private void showDataColumn(int dataIndex) {
+ if (dataIndex < 0 || dataIndex >= columns.length) {
+ return;
+ }
+ hiddenDataColumns.remove(dataIndex);
+ TableColumn tc = tableColumn[dataIndex + 1];
+ if (tc == null || tc.isDisposed()) {
+ return;
+ }
+ tc.setResizable(true);
+ int restore = rememberedWidths != null ? rememberedWidths[dataIndex + 1] :
0;
+ if (restore > 0) {
+ tc.setWidth(restore);
+ } else if (tc.getWidth() <= 0) {
+ tc.pack();
+ }
+ }
+
+ private void applyHiddenAndOrder(List<Integer> visibleDataIndices) {
+ for (int i = 0; i < columns.length; i++) {
+ TableColumn tc = tableColumn[i + 1];
+ if (tc == null || tc.isDisposed()) {
+ continue;
+ }
+ if (hiddenDataColumns.contains(i)) {
+ rememberWidth(i + 1);
+ tc.setWidth(0);
+ tc.setResizable(false);
+ } else {
+ tc.setResizable(true);
+ if (tc.getWidth() <= 0) {
+ int restore = rememberedWidths != null ? rememberedWidths[i + 1] : 0;
+ tc.setWidth(restore > 0 ? restore : 50);
+ }
+ }
+ }
+
+ int[] order = new int[tableColumn.length];
+ int pos = 0;
+ order[pos++] = 0;
+ for (int dataIdx : visibleDataIndices) {
+ order[pos++] = dataIdx + 1;
+ }
+ for (int i = 0; i < columns.length; i++) {
+ if (hiddenDataColumns.contains(i)) {
+ order[pos++] = i + 1;
+ }
+ }
+ setColumnOrderSafe(order);
+ }
+
+ private void ensureRememberedWidths() {
+ if (rememberedWidths != null && rememberedWidths.length ==
tableColumn.length) {
+ return;
+ }
+ rememberedWidths = new int[tableColumn.length];
+ for (int i = 0; i < tableColumn.length; i++) {
+ if (tableColumn[i] != null && !tableColumn[i].isDisposed()) {
+ rememberedWidths[i] = tableColumn[i].getWidth();
+ }
+ }
+ }
+
+ private void rememberWidth(int tableColIndex) {
+ ensureRememberedWidths();
+ TableColumn tc = tableColumn[tableColIndex];
+ if (tc == null || tc.isDisposed()) {
+ return;
+ }
+ int width = tc.getWidth();
+ if (width > 0) {
+ rememberedWidths[tableColIndex] = width;
+ }
+ }
+
+ private int[] naturalColumnOrder() {
+ int[] order = new int[tableColumn.length];
+ for (int i = 0; i < order.length; i++) {
+ order[i] = i;
+ }
+ return order;
+ }
+
+ private int[] getColumnOrderSafe() {
+ try {
+ int[] order = table.getColumnOrder();
+ if (order != null && order.length == tableColumn.length) {
+ return order;
+ }
+ } catch (Exception e) {
+ // RAP / some SWT ports do not implement column order
+ }
+ return naturalColumnOrder();
+ }
+
+ private void setColumnOrderSafe(int[] order) {
+ try {
+ table.setColumnOrder(order);
+ } catch (Exception e) {
+ // RAP / some SWT ports do not implement column order
+ }
+ }
+
+ private void addHeaderContextMenu(Menu rowMenu) {
+ Menu headerMenu = new Menu(table);
+
+ MenuItem miHide = new MenuItem(headerMenu, SWT.NONE);
+ miHide.setText(
+ OsHelper.customizeMenuitemText(BaseMessages.getString(PKG,
"TableView.menu.HideColumn")));
+ final int[] headerDataColumn = {-1};
+ miHide.addListener(
+ SWT.Selection,
+ e -> {
+ if (headerDataColumn[0] >= 0) {
+ hideDataColumn(headerDataColumn[0]);
+ }
+ });
+
+ MenuItem miShowAll = new MenuItem(headerMenu, SWT.NONE);
+ miShowAll.setText(
+ OsHelper.customizeMenuitemText(
+ BaseMessages.getString(PKG, "TableView.menu.ShowAllColumns")));
+ miShowAll.addListener(SWT.Selection, e -> resetColumnView());
+
+ if (!removeToolItems.contains(ID_TOOLBAR_TABLE_VIEWS)) {
+ new MenuItem(headerMenu, SWT.SEPARATOR);
+ MenuItem miTableViews = new MenuItem(headerMenu, SWT.NONE);
+ miTableViews.setText(
+ OsHelper.customizeMenuitemText(BaseMessages.getString(PKG,
"TableView.menu.TableViews")));
+ miTableViews.setImage(GuiResource.getInstance().getImageView());
+ miTableViews.addListener(SWT.Selection, e -> editTableViews());
+ }
+
+ table.addListener(
+ SWT.MenuDetect,
+ event -> {
+ Point pt = table.toControl(event.x, event.y);
+ boolean onHeader = pt.y >= 0 && pt.y < table.getHeaderHeight();
+ if (onHeader) {
+ headerDataColumn[0] = findDataColumnAtX(pt.x);
+ miHide.setEnabled(headerDataColumn[0] >= 0);
+ table.setMenu(headerMenu);
+ } else {
+ table.setMenu(rowMenu);
+ }
+ });
+ }
+
+ private int findDataColumnAtX(int x) {
+ int pos = 0;
+ ScrollBar hBar = table.getHorizontalBar();
+ if (hBar != null && !hBar.isDisposed()) {
+ pos -= hBar.getSelection();
+ }
+ for (int tableIdx : getColumnOrderSafe()) {
+ TableColumn tc = table.getColumn(tableIdx);
+ int width = tc.getWidth();
+ if (x >= pos && x < pos + width) {
+ return tableIdx == 0 ? -1 : tableIdx - 1;
+ }
+ pos += width;
+ }
+ return -1;
+ }
+
public IRowMeta getRowWithoutValues() {
IRowMeta f = new RowMeta();
f.addValueMeta(new ValueMetaInteger("#"));
diff --git a/engine/src/main/java/org/apache/hop/history/AuditStateMap.java
b/ui/src/main/java/org/apache/hop/ui/core/widget/TableViewColumnView.java
similarity index 52%
copy from engine/src/main/java/org/apache/hop/history/AuditStateMap.java
copy to ui/src/main/java/org/apache/hop/ui/core/widget/TableViewColumnView.java
index 96ab82171a..1286aeeda3 100644
--- a/engine/src/main/java/org/apache/hop/history/AuditStateMap.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/widget/TableViewColumnView.java
@@ -14,41 +14,24 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.hop.history;
+package org.apache.hop.ui.core.widget;
-import java.util.HashMap;
-import java.util.Map;
+import java.util.ArrayList;
+import java.util.List;
import lombok.Getter;
import lombok.Setter;
-@Setter
+/** A named, reusable subset and order of {@link TableView} columns. */
@Getter
-public class AuditStateMap {
-
- private Map<String, AuditState> nameStateMap;
-
- public AuditStateMap() {
- nameStateMap = new HashMap<>();
- }
-
- public AuditStateMap(Map<String, AuditState> nameStateMap) {
- this.nameStateMap = nameStateMap;
- }
+@Setter
+public class TableViewColumnView {
+ private String name;
+ private List<String> columnNames = new ArrayList<>();
- public void add(AuditState auditState) {
- AuditState existing = get(auditState.getName());
- if (existing != null) {
- // Add the states to the existing states
- //
- existing.getStateMap().putAll(auditState.getStateMap());
- } else {
- // Store a new set of properties
- //
- nameStateMap.put(auditState.getName(), auditState);
- }
- }
+ public TableViewColumnView() {}
- public AuditState get(String name) {
- return nameStateMap.get(name);
+ public TableViewColumnView(String name, List<String> columnNames) {
+ this.name = name;
+ this.columnNames = columnNames != null ? new ArrayList<>(columnNames) :
new ArrayList<>();
}
}
diff --git
a/ui/src/main/java/org/apache/hop/ui/core/widget/TableViewColumnViewManager.java
b/ui/src/main/java/org/apache/hop/ui/core/widget/TableViewColumnViewManager.java
new file mode 100644
index 0000000000..76d206544d
--- /dev/null
+++
b/ui/src/main/java/org/apache/hop/ui/core/widget/TableViewColumnViewManager.java
@@ -0,0 +1,149 @@
+/*
+ * 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.core.widget;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hop.core.logging.LogChannel;
+import org.apache.hop.history.AuditManager;
+import org.apache.hop.history.AuditState;
+import org.apache.hop.history.AuditStateMap;
+
+/**
+ * Load and store named {@link TableView} column views via the active {@link
AuditManager}.
+ *
+ * <p>Each view is one {@link AuditState} under type {@link #AUDIT_TYPE}. The
group is the current
+ * project namespace.
+ */
+public final class TableViewColumnViewManager {
+ public static final String AUDIT_TYPE = "table-view";
+ public static final String STATE_COLUMNS = "columns";
+
+ private TableViewColumnViewManager() {}
+
+ public static List<TableViewColumnView> list(String group) {
+ List<TableViewColumnView> views = new ArrayList<>();
+ if (StringUtils.isEmpty(group)) {
+ return views;
+ }
+ try {
+ AuditStateMap stateMap =
AuditManager.getActive().loadAuditStateMap(group, AUDIT_TYPE);
+ if (stateMap == null || stateMap.getNameStateMap() == null) {
+ return views;
+ }
+ for (AuditState state : stateMap.getNameStateMap().values()) {
+ TableViewColumnView view = fromState(state);
+ if (view != null && StringUtils.isNotEmpty(view.getName())) {
+ views.add(view);
+ }
+ }
+ views.sort(
+ Comparator.comparing(
+ view -> view.getName() == null ? "" : view.getName(),
String.CASE_INSENSITIVE_ORDER));
+ } catch (Exception e) {
+ LogChannel.UI.logError(
+ "Unable to list table views from audit manager type " + AUDIT_TYPE +
" in group " + group,
+ e);
+ }
+ return views;
+ }
+
+ public static TableViewColumnView load(String group, String name) {
+ if (StringUtils.isEmpty(group) || StringUtils.isEmpty(name)) {
+ return null;
+ }
+ try {
+ AuditState state = AuditManager.getActive().retrieveState(group,
AUDIT_TYPE, name);
+ return fromState(state);
+ } catch (Exception e) {
+ LogChannel.UI.logError(
+ "Unable to load table view '"
+ + name
+ + "' from audit manager type "
+ + AUDIT_TYPE
+ + " in group "
+ + group,
+ e);
+ return null;
+ }
+ }
+
+ public static void save(String group, TableViewColumnView view) {
+ if (StringUtils.isEmpty(group) || view == null ||
StringUtils.isEmpty(view.getName())) {
+ return;
+ }
+ Map<String, Object> stateMap = new HashMap<>();
+ List<String> columns =
+ view.getColumnNames() != null ? new ArrayList<>(view.getColumnNames())
: new ArrayList<>();
+ stateMap.put(STATE_COLUMNS, columns);
+ AuditManager.storeState(LogChannel.UI, group, AUDIT_TYPE, view.getName(),
stateMap);
+ }
+
+ public static void delete(String group, String name) {
+ if (StringUtils.isEmpty(group) || StringUtils.isEmpty(name)) {
+ return;
+ }
+ try {
+ AuditStateMap stateMap =
AuditManager.getActive().loadAuditStateMap(group, AUDIT_TYPE);
+ if (stateMap == null) {
+ return;
+ }
+ stateMap.remove(name);
+ AuditManager.getActive().saveAuditStateMap(group, AUDIT_TYPE, stateMap);
+ } catch (Exception e) {
+ LogChannel.UI.logError(
+ "Unable to delete table view '"
+ + name
+ + "' from audit manager type "
+ + AUDIT_TYPE
+ + " in group "
+ + group,
+ e);
+ }
+ }
+
+ static TableViewColumnView fromState(AuditState state) {
+ if (state == null || StringUtils.isEmpty(state.getName())) {
+ return null;
+ }
+ TableViewColumnView view = new TableViewColumnView();
+ view.setName(state.getName());
+ view.setColumnNames(extractStringList(state, STATE_COLUMNS));
+ return view;
+ }
+
+ static List<String> extractStringList(AuditState state, String key) {
+ List<String> columns = new ArrayList<>();
+ if (state == null || state.getStateMap() == null) {
+ return columns;
+ }
+ Object raw = state.getStateMap().get(key);
+ if (raw instanceof Collection<?> collection) {
+ for (Object item : collection) {
+ if (item != null) {
+ columns.add(String.valueOf(item));
+ }
+ }
+ }
+ return columns;
+ }
+}
diff --git
a/ui/src/main/java/org/apache/hop/ui/core/widget/TableViewColumnViews.java
b/ui/src/main/java/org/apache/hop/ui/core/widget/TableViewColumnViews.java
new file mode 100644
index 0000000000..52833a7cf6
--- /dev/null
+++ b/ui/src/main/java/org/apache/hop/ui/core/widget/TableViewColumnViews.java
@@ -0,0 +1,63 @@
+/*
+ * 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.core.widget;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Matching helpers for named {@link TableView} column views. Kept free of SWT
so the logic can be
+ * unit tested.
+ */
+public final class TableViewColumnViews {
+ private TableViewColumnViews() {}
+
+ /**
+ * Resolve view column names to indices into {@code availableNames}.
+ *
+ * <p>Matching is case-sensitive (Hop field names). Duplicate available
names take the first
+ * unused match. Names that are not present are skipped.
+ *
+ * @param availableNames column names of the current table, in creation order
+ * @param viewNames column names in the desired display order
+ * @return matching indices into {@code availableNames}, in view order
+ */
+ public static List<Integer> resolveColumnIndices(
+ String[] availableNames, List<String> viewNames) {
+ List<Integer> indices = new ArrayList<>();
+ if (availableNames == null || viewNames == null) {
+ return indices;
+ }
+ boolean[] used = new boolean[availableNames.length];
+ for (String viewName : viewNames) {
+ if (viewName == null) {
+ continue;
+ }
+ for (int i = 0; i < availableNames.length; i++) {
+ if (used[i]) {
+ continue;
+ }
+ if (viewName.equals(availableNames[i])) {
+ used[i] = true;
+ indices.add(i);
+ break;
+ }
+ }
+ }
+ return indices;
+ }
+}
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 56abaa9ee6..79d1a138a7 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
@@ -319,6 +319,31 @@ ShowRowsDialog.CellTooltip.Type=Type: {0}
ShowRowsDialog.CellTooltip.Length=Length: {0}
ShowRowsDialog.CellTooltip.Precision=Precision: {0}
ShowRowsDialog.CellTooltip.Origin=Origin: {0}
+TableViewColumnViewDialog.AddAll.ToolTip=Add all available columns to the view
+TableViewColumnViewDialog.AddOne.ToolTip=Add the selected columns to the view
+TableViewColumnViewDialog.Apply.Label=&Apply
+TableViewColumnViewDialog.Available.Label=Available columns
+TableViewColumnViewDialog.Delete.Message=Delete view ''{0}''?
+TableViewColumnViewDialog.Delete.Title=Delete view
+TableViewColumnViewDialog.EmptyView.Message=Select at least one column to
apply a view.
+TableViewColumnViewDialog.MoveDown.Label=Move down
+TableViewColumnViewDialog.MoveUp.Label=Move up
+TableViewColumnViewDialog.Name.Empty=Please enter a name for the view.
+TableViewColumnViewDialog.Name.Message=Name of the table view
+TableViewColumnViewDialog.Name.Title=View name
+TableViewColumnViewDialog.NoMatch.Message=None of the fields in this view
exist in the current table.
+TableViewColumnViewDialog.Overwrite.Message=A view named ''{0}'' already
exists. Overwrite it?
+TableViewColumnViewDialog.Overwrite.Title=Overwrite view
+TableViewColumnViewDialog.RemoveAll.ToolTip=Remove all columns from the view
+TableViewColumnViewDialog.RemoveOne.ToolTip=Remove the selected columns from
the view
+TableViewColumnViewDialog.Rename.Label=&Rename
+TableViewColumnViewDialog.ShowAll.Label=Show all columns
+TableViewColumnViewDialog.Shown.Label=Columns in view (display order)
+TableViewColumnViewDialog.Status.Missing={0} field(s) not in this table
+TableViewColumnViewDialog.Status.Visible={0} of {1} columns visible
+TableViewColumnViewDialog.Title=Table views
+TableViewColumnViewDialog.UseCurrent.Label=Use current layout
+TableViewColumnViewDialog.Views.Label=Views
ProgressMonitorDialog.InitialTaskLabel=Apache Hop is handling a long running
task
ProgressMonitorDialog.Shell.Title=Progress...
ProgressMonitorDialog.InitialSubTaskLabel=\
diff --git
a/ui/src/main/resources/org/apache/hop/ui/core/widget/messages/messages_en_US.properties
b/ui/src/main/resources/org/apache/hop/ui/core/widget/messages/messages_en_US.properties
index 2d4575ff85..1b14a66d05 100644
---
a/ui/src/main/resources/org/apache/hop/ui/core/widget/messages/messages_en_US.properties
+++
b/ui/src/main/resources/org/apache/hop/ui/core/widget/messages/messages_en_US.properties
@@ -70,7 +70,10 @@ TableView.menu.InsertBeforeRow=Insert before this row
TableView.menu.KeepSelected=Keep only selected lines\tCtrl-K
TableView.menu.MoveDown=Move down\tCtrl-DOWN
TableView.menu.MoveUp=Move up\tCtrl-UP
+TableView.menu.HideColumn=Hide column
TableView.menu.NavigateToColumn=Navigate to column
+TableView.menu.ShowAllColumns=Show all columns
+TableView.menu.TableViews=Table views...
TableView.menu.OptimalSizeWithHeader=Optimal Column size incl. header\tF3
TableView.menu.OptimalSizeWithoutHeader=Optimal Column size excl. header\tF4
TableView.menu.PasteFromClipboard=Paste clipboard to table\tCtrl-V
@@ -96,6 +99,7 @@ TableView.WebNewlineHint.Label=* Line breaks aren't shown in
table cells in the
TableView.ToolBarWidget.MoveRowsDown.ToolTip=Move selected rows down
TableView.ToolBarWidget.MoveRowsUp.ToolTip=Move selected rows up
TableView.ToolBarWidget.NavigateToColumn.ToolTip=Navigate to a column
+TableView.ToolBarWidget.TableViews.ToolTip=Table views
TableView.tooltip.ExpandValue=Edit this value in a multi-line editor
(Shift+Enter)
TableView.tooltip.ViewValue=Show the full value in a multi-line viewer
(Shift+Enter)
TableView.tooltip.MultilineEditorKeys=Enter\: save and close - Shift+Enter\:
new line - Esc\: cancel
diff --git
a/ui/src/test/java/org/apache/hop/ui/core/widget/TableViewColumnViewManagerTest.java
b/ui/src/test/java/org/apache/hop/ui/core/widget/TableViewColumnViewManagerTest.java
new file mode 100644
index 0000000000..a10eb630ee
--- /dev/null
+++
b/ui/src/test/java/org/apache/hop/ui/core/widget/TableViewColumnViewManagerTest.java
@@ -0,0 +1,78 @@
+/*
+ * 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.core.widget;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.nio.file.Path;
+import java.util.List;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.history.AuditManager;
+import org.apache.hop.history.local.LocalAuditManager;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+class TableViewColumnViewManagerTest {
+
+ @TempDir Path testFolder;
+
+ @BeforeEach
+ void resetAuditManager() throws HopException {
+ AuditManager.getInstance()
+ .setActiveAuditManager(new
LocalAuditManager(testFolder.toAbsolutePath().toString()));
+ }
+
+ @Test
+ void saveListLoadAndDelete() {
+ String group = "test-project";
+
+ TableViewColumnViewManager.save(
+ group, new TableViewColumnView("Customer keys", List.of("id", "name",
"email")));
+ TableViewColumnViewManager.save(
+ group, new TableViewColumnView("Order dates", List.of("order_id",
"created_at")));
+
+ List<TableViewColumnView> views = TableViewColumnViewManager.list(group);
+ assertEquals(2, views.size());
+ assertEquals("Customer keys", views.get(0).getName());
+ assertEquals("Order dates", views.get(1).getName());
+
+ TableViewColumnView loaded = TableViewColumnViewManager.load(group,
"Customer keys");
+ assertEquals(List.of("id", "name", "email"), loaded.getColumnNames());
+
+ TableViewColumnViewManager.save(
+ group, new TableViewColumnView("Customer keys", List.of("id",
"email")));
+ loaded = TableViewColumnViewManager.load(group, "Customer keys");
+ assertEquals(List.of("id", "email"), loaded.getColumnNames());
+
+ TableViewColumnViewManager.delete(group, "Customer keys");
+ assertNull(TableViewColumnViewManager.load(group, "Customer keys"));
+ views = TableViewColumnViewManager.list(group);
+ assertEquals(1, views.size());
+ assertEquals("Order dates", views.get(0).getName());
+ }
+
+ @Test
+ void emptyGroupReturnsNothing() {
+ assertTrue(TableViewColumnViewManager.list("").isEmpty());
+ assertTrue(TableViewColumnViewManager.list(null).isEmpty());
+ assertNull(TableViewColumnViewManager.load("group", null));
+ assertNull(TableViewColumnViewManager.load(null, "name"));
+ }
+}
diff --git
a/ui/src/test/java/org/apache/hop/ui/core/widget/TableViewColumnViewsTest.java
b/ui/src/test/java/org/apache/hop/ui/core/widget/TableViewColumnViewsTest.java
new file mode 100644
index 0000000000..150d5451d9
--- /dev/null
+++
b/ui/src/test/java/org/apache/hop/ui/core/widget/TableViewColumnViewsTest.java
@@ -0,0 +1,75 @@
+/*
+ * 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.core.widget;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+
+class TableViewColumnViewsTest {
+
+ @Test
+ void resolvesNamesInViewOrder() {
+ String[] available = {"id", "name", "email", "amount"};
+ List<Integer> indices =
+ TableViewColumnViews.resolveColumnIndices(available, List.of("email",
"id"));
+
+ assertEquals(List.of(2, 0), indices);
+ }
+
+ @Test
+ void skipsMissingNames() {
+ String[] available = {"id", "name", "email"};
+ List<Integer> indices =
+ TableViewColumnViews.resolveColumnIndices(
+ available, List.of("email", "missing", "id", "also_missing"));
+
+ assertEquals(List.of(2, 0), indices);
+ }
+
+ @Test
+ void usesFirstUnusedMatchForDuplicateAvailableNames() {
+ String[] available = {"id", "name", "id"};
+ List<Integer> indices =
+ TableViewColumnViews.resolveColumnIndices(available, List.of("id",
"id"));
+
+ assertEquals(List.of(0, 2), indices);
+ }
+
+ @Test
+ void emptyViewOrTableYieldsEmptyResult() {
+ assertTrue(TableViewColumnViews.resolveColumnIndices(new String[0],
List.of("id")).isEmpty());
+ assertTrue(
+ TableViewColumnViews.resolveColumnIndices(new String[] {"id"},
Collections.emptyList())
+ .isEmpty());
+ assertTrue(TableViewColumnViews.resolveColumnIndices(null,
List.of("id")).isEmpty());
+ assertTrue(TableViewColumnViews.resolveColumnIndices(new String[] {"id"},
null).isEmpty());
+ }
+
+ @Test
+ void zeroMatchesIsEmpty() {
+ List<Integer> indices =
+ TableViewColumnViews.resolveColumnIndices(
+ new String[] {"id", "name"}, Arrays.asList("other", null));
+
+ assertTrue(indices.isEmpty());
+ }
+}