From 9799cebac82a28154ef14fb362a9b782422f5777 Mon Sep 17 00:00:00 2001 From: hengsin Date: Wed, 10 Apr 2024 20:21:24 +0800 Subject: [PATCH] IDEMPIERE-6099 Global Search Enhancements (#2305) * IDEMPIERE-6099 Global Search Enhancements * IDEMPIERE-6099 Global Search Enhancements --- .../webui/apps/DocumentSearchController.java | 99 +++++++++++++++++-- .../adempiere/webui/apps/GlobalSearch.java | 37 +++++-- .../webui/apps/MenuSearchController.java | 39 ++++++++ .../adempiere/webui/panel/HeaderPanel.java | 17 ++-- .../css/fragment/application-menu.css.dsp | 13 +++ 5 files changed, 184 insertions(+), 21 deletions(-) diff --git a/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/apps/DocumentSearchController.java b/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/apps/DocumentSearchController.java index 5fca715073..6cc82161fa 100644 --- a/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/apps/DocumentSearchController.java +++ b/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/apps/DocumentSearchController.java @@ -21,6 +21,7 @@ import java.util.Collections; import java.util.Comparator; import java.util.List; +import org.adempiere.webui.LayoutUtils; import org.adempiere.webui.component.Label; import org.adempiere.webui.util.ZKUpdateUtil; import org.compiere.model.I_AD_SearchDefinition; @@ -38,6 +39,7 @@ import org.compiere.model.Query; import org.compiere.util.DB; import org.compiere.util.DisplayType; import org.compiere.util.Env; +import org.compiere.util.Msg; import org.compiere.util.Util; import org.zkoss.zk.ui.Component; import org.zkoss.zk.ui.event.Event; @@ -52,7 +54,9 @@ import org.zkoss.zul.Vlayout; * */ public class DocumentSearchController implements EventListener{ - + + /** Style for transaction code guide or execution error */ + private static final String MESSAGE_LABEL_STYLE = "color: rgba(0,0,0,0.34)"; /** {@link A} component attribute to hold reference to corresponding {@link #SEARCH_RESULT} **/ private static final String SEARCH_RESULT = "search.result"; /** onSearchDocuments event **/ @@ -64,6 +68,8 @@ public class DocumentSearchController implements EventListener{ private ArrayList list; /** Current selected index of {@link #list} **/ private int selected = -1; + /** True when showing transaction code available */ + private boolean showingGuide = false; /** * default constructor @@ -92,7 +98,12 @@ public class DocumentSearchController implements EventListener{ * @param value */ public void search(String value) { - layout.getChildren().clear(); + if (Util.isEmpty(value) || (value.startsWith("/") && value.indexOf(" ") < 0)) { + if (!showingGuide) + layout.getChildren().clear(); + } else { + layout.getChildren().clear(); + } Events.echoEvent(ON_SEARCH_DOCUMENTS_EVENT, layout, value); } @@ -103,12 +114,31 @@ public class DocumentSearchController implements EventListener{ */ private void onSearchDocuments(String searchString) { list = new ArrayList(); - if (Util.isEmpty(searchString)) { + if (Util.isEmpty(searchString) || (searchString.startsWith("/") && searchString.indexOf(" ") < 0)) { + // No search string, show available transaction code + if (!showingGuide) { + Query query = new Query(Env.getCtx(), I_AD_SearchDefinition.Table_Name, "TransactionCode IS NOT NULL", null); + List definitions = query.setOnlyActiveRecords(true).setOrderBy("TransactionCode").list(); + for(MSearchDefinition definition : definitions) { + Label label = new Label("/"+definition.getTransactionCode() + " " + definition.getName()); + label.setStyle(MESSAGE_LABEL_STYLE); + layout.appendChild(label); + } + showingGuide = true; + } return; } + showingGuide = false; + // Search and show results List list = doSearch(searchString); - if (list.size() > 0) { + + if (list.size() == 1 && list.get(0).getRecordId() == -1) { + // DB error or query timeout + Label label = new Label(list.get(0).getLabel()); + label.setStyle(MESSAGE_LABEL_STYLE); + layout.appendChild(label); + } else if (list.size() > 0) { Collections.sort(list, new Comparator() { @Override public int compare(SearchResult o1, SearchResult o2) { @@ -118,20 +148,47 @@ public class DocumentSearchController implements EventListener{ return r; } }); + + String matchString = searchString.toLowerCase(); + if (searchString != null && searchString.startsWith("/") && searchString.indexOf(" ") > 1) { + // "/TransactionCode Search Text" + matchString = searchString.substring(searchString.indexOf(" ")+1).toLowerCase(); + } + String windowName = null; for(SearchResult result : list) { if (windowName == null || !windowName.equals(result.getWindowName())) { windowName = result.getWindowName(); Label label = new Label(windowName); - label.setStyle("padding: 3px; font-weight: bold; display: inline-block;"); + LayoutUtils.addSclass("window-name", label); layout.appendChild(label); } A a = new A(); a.setAttribute(SEARCH_RESULT, result); - a.setLabel(result.getLabel()); layout.appendChild(a); - a.setStyle("padding-left: 3px; display: inline-block;"); + LayoutUtils.addSclass("search-result", a); a.addEventListener(Events.ON_CLICK, this); + String label = result.getLabel(); + if (!Util.isEmpty(matchString, true)) { + int match = label.toLowerCase().indexOf(matchString); + while (match >= 0) { + if (match > 0) { + a.appendChild(new Label(label.substring(0, match))); + Label l = new Label(label.substring(match, match+matchString.length())); + LayoutUtils.addSclass("highlight", l); + a.appendChild(l); + label = label.substring(match+matchString.length()); + } else { + Label l = new Label(label.substring(0, matchString.length())); + LayoutUtils.addSclass("highlight", l); + a.appendChild(l); + label = label.substring(matchString.length()); + } + match = label.toLowerCase().indexOf(matchString); + } + } + if (label.length() > 0) + a.appendChild(new Label(label)); } layout.invalidate(); } @@ -146,7 +203,23 @@ public class DocumentSearchController implements EventListener{ final MRole role = MRole.get(Env.getCtx(), Env.getAD_Role_ID(Env.getCtx()), Env.getAD_User_ID(Env.getCtx()), true); selected = -1; - Query query = new Query(Env.getCtx(), I_AD_SearchDefinition.Table_Name, "TransactionCode IS NULL", null); + + // Search with or without transaction code + StringBuilder whereClause = new StringBuilder(); + String transactionCode = null; + if (searchString != null && searchString.startsWith("/") && searchString.indexOf(" ") > 1) { + // "/TransactionCode Search Text" + transactionCode = searchString.substring(1, searchString.indexOf(" ")); + searchString = searchString.substring(searchString.indexOf(" ")+1); + whereClause.append("Upper(TransactionCode) = ?"); + } else { + // Search with definition that doesn't use transaction code + whereClause.append("TransactionCode IS NULL"); + } + + Query query = new Query(Env.getCtx(), I_AD_SearchDefinition.Table_Name, whereClause.toString(), null); + if (transactionCode != null) + query.setParameters(transactionCode.toUpperCase()); List definitions = query.setOnlyActiveRecords(true).list(); for(MSearchDefinition msd : definitions) { MTable table = new MTable(Env.getCtx(), msd.getAD_Table_ID(), null); @@ -270,7 +343,15 @@ public class DocumentSearchController implements EventListener{ list.add(result); } } catch (SQLException e) { - e.printStackTrace(); + SearchResult result = new SearchResult(); + result.setRecordId(-1); + if (DB.getDatabase().isQueryTimeout(e)) { + result.setLabel(Msg.getMsg(Env.getCtx(), "Timeout")); + } else { + result.setLabel(Msg.getMsg(Env.getCtx(), "DBExecuteError")); + e.printStackTrace(); + } + list.add(result); } finally { DB.close(rs, pstmt); } diff --git a/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/apps/GlobalSearch.java b/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/apps/GlobalSearch.java index 59aaecc69b..8876e6ce6d 100644 --- a/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/apps/GlobalSearch.java +++ b/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/apps/GlobalSearch.java @@ -100,9 +100,13 @@ public class GlobalSearch extends Div implements EventListener { bandbox.addEventListener(Events.ON_CHANGE, this); bandbox.setCtrlKeys("#up#down"); bandbox.addEventListener(Events.ON_CTRL_KEY, this); + bandbox.addEventListener(Events.ON_FOCUS, e -> { + if (!bandbox.isOpen()) + bandbox.setOpen(true); + }); Bandpopup popup = new Bandpopup(); - ZKUpdateUtil.setWindowHeightX(popup, ClientInfo.get().desktopHeight-50); + ZKUpdateUtil.setWindowHeightX(popup, ClientInfo.get().desktopHeight-100); bandbox.appendChild(popup); tabbox = new Tabbox(); @@ -139,15 +143,18 @@ public class GlobalSearch extends Div implements EventListener { @Override public void onEvent(Event event) throws Exception { if (Events.ON_CHANGING.equals(event.getName())) { - //post ON_SEARCH_EVENT for ON_CHANGING from bandbox + // Post ON_SEARCH_EVENT for ON_CHANGING from bandbox InputEvent inputEvent = (InputEvent) event; - String value = inputEvent.getValue(); + String value = inputEvent.getValue(); + // Auto switch to Search with "/" + if (value != null && value.startsWith("/") && tabbox.getSelectedIndex()==0) + tabbox.setSelectedIndex(1); bandbox.setAttribute(LAST_ONCHANGING_ATTR, value); Events.postEvent(ON_SEARCH_EVENT, this, value); } else if (Events.ON_CHANGE.equals(event.getName())) { bandbox.removeAttribute(LAST_ONCHANGING_ATTR); } else if (Events.ON_CTRL_KEY.equals(event.getName())) { - //handle keyboard navigation for bandbox items + // Handle keyboard navigation for bandbox items KeyEvent ke = (KeyEvent) event; if (ke.getKeyCode() == KeyEvent.UP) { if (bandbox.getFirstChild().isVisible()) { @@ -180,10 +187,12 @@ public class GlobalSearch extends Div implements EventListener { } } else if (event.getName().equals(ON_SEARCH_EVENT)) { String value = (String) event.getData(); - if (tabbox.getSelectedIndex()==0) + if (tabbox.getSelectedIndex()==0) { + menuController.setHighlightText(value); menuController.search(value); - else + } else { docController.search(value); + } bandbox.focus(); } else if (event.getName().equals(ON_CREATE_ECHO_EVENT)) { //setup client side listener for enter key @@ -247,4 +256,20 @@ public class GlobalSearch extends Div implements EventListener { public void onClientInfo() { ZKUpdateUtil.setWindowHeightX(bandbox.getDropdown(), ClientInfo.get().desktopHeight-50); } + + /** + * Set place holder text for global search input box + * @param placeHolder + */ + public void setPlaceHolderText(String placeHolder) { + bandbox.setPlaceholder(placeHolder); + } + + /** + * Set tooltip text for global search input box + * @param tooltipText + */ + public void setTooltipText(String tooltipText) { + bandbox.setTooltiptext(tooltipText); + } } diff --git a/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/apps/MenuSearchController.java b/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/apps/MenuSearchController.java index e764f2c83a..28530e2dbb 100644 --- a/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/apps/MenuSearchController.java +++ b/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/apps/MenuSearchController.java @@ -17,6 +17,8 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.List; +import org.adempiere.webui.LayoutUtils; +import org.adempiere.webui.component.Label; import org.adempiere.webui.component.ListHead; import org.adempiere.webui.component.ListItem; import org.adempiere.webui.component.Listbox; @@ -95,6 +97,8 @@ public class MenuSearchController implements EventListener{ private ListModelList fullModel; /** true when controller is handling event from Star/Favourite button **/ private boolean inStarEvent; + + private String highlightText = null; /** Event post from {@link #selectTreeitem(Object, Boolean)} **/ private static final String ON_POST_SELECT_TREEITEM_EVENT = "onPostSelectTreeitem"; @@ -604,6 +608,14 @@ public class MenuSearchController implements EventListener{ return false; } + /** + * Set text to highlight + * @param s + */ + public void setHighlightText(String s) { + highlightText = s; + } + /** * {@link ListitemRenderer} for {@link #listbox} */ @@ -634,6 +646,33 @@ public class MenuSearchController implements EventListener{ cell.setImage(null); cell.setIconSclass(data.getImage()); } + + // Highlight search text + if (!Util.isEmpty(highlightText, true) && data.getLabel().toLowerCase().contains(highlightText.toLowerCase())) { + // Space to maintain proper gap between icon and label + cell.setLabel(" "); + String label = data.getLabel(); + String matchString = highlightText.toLowerCase(); + int match = label.toLowerCase().indexOf(matchString); + while (match >= 0) { + if (match > 0) { + cell.appendChild(new Label(label.substring(0, match))); + Label l = new Label(label.substring(match, match+matchString.length())); + LayoutUtils.addSclass("highlight", l); + cell.appendChild(l); + label = label.substring(match+matchString.length()); + } else { + Label l = new Label(label.substring(0, matchString.length())); + LayoutUtils.addSclass("highlight", l); + cell.appendChild(l); + label = label.substring(matchString.length()); + } + match = label.toLowerCase().indexOf(matchString); + } + if (label.length() > 0) + cell.appendChild(new Label(label)); + } + item.appendChild(cell); cell.setTooltip(data.getDescription()); item.setValue(data); diff --git a/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/panel/HeaderPanel.java b/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/panel/HeaderPanel.java index 43f7a75d33..140e07f676 100644 --- a/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/panel/HeaderPanel.java +++ b/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/panel/HeaderPanel.java @@ -95,7 +95,7 @@ public class HeaderPanel extends Panel implements EventListener btnMenu = (LabelImageElement) getFellow("menuButton"); btnMenu.setIconSclass("z-icon-sitemap"); - btnMenu.setTooltiptext(Util.cleanAmp(Msg.getMsg(Env.getCtx(),"Menu"))); + btnMenu.setTooltiptext(Util.cleanAmp(Msg.getMsg(Env.getCtx(),"Menu")) + " Alt+M"); btnMenu.addEventListener(Events.ON_CLICK, this); if (ClientInfo.isMobile()) { LayoutUtils.addSclass("mobile", this); @@ -131,6 +131,8 @@ public class HeaderPanel extends Panel implements EventListener stub.getParent().insertBefore(globalSearch, stub); stub.detach(); globalSearch.setId("menuLookup"); + globalSearch.setPlaceHolderText("Alt+G"); + globalSearch.setTooltipText("Alt+G"); } @Override @@ -154,16 +156,19 @@ public class HeaderPanel extends Panel implements EventListener } else if (Events.ON_CREATE.equals(event.getName())) { onCreate(); }else if (event instanceof KeyEvent) - { - //alt+m for the menu + { KeyEvent ke = (KeyEvent) event; - if (ke.getKeyCode() == 77) + if (ke.getKeyCode() == 77) // alt+m for the menu { popMenu.open(btnMenu, "after_start"); popMenu.setFocus(true); - }else if (ke.getKeyCode() == 27) { + } + else if (ke.getKeyCode() == 27) // esc to close menu + { popMenu.close(); - }else if (ke.getKeyCode() == 71) { + } + else if (ke.getKeyCode() == 71) // alt+g for the search + { globalSearch.setFocus(true); } } else if(event.getName().equals(ZoomEvent.EVENT_NAME)) { diff --git a/org.adempiere.ui.zk/WEB-INF/src/web/theme/default/css/fragment/application-menu.css.dsp b/org.adempiere.ui.zk/WEB-INF/src/web/theme/default/css/fragment/application-menu.css.dsp index 8ddb702fe8..ada5f5cff4 100644 --- a/org.adempiere.ui.zk/WEB-INF/src/web/theme/default/css/fragment/application-menu.css.dsp +++ b/org.adempiere.ui.zk/WEB-INF/src/web/theme/default/css/fragment/application-menu.css.dsp @@ -152,6 +152,19 @@ } } +.global-search-tabpanel .window-name.z-label { + padding: 3px; + font-weight: bold; + display: inline-block; +} +.global-search-tabpanel .search-result.z-a { + padding-left: 3px; + display: inline-block; +} +.global-search-tabpanel .highlight { + background-color: #FFFF00; +} + .menu-href [class^="z-icon-"] { font-size: larger; color: #333;