IDEMPIERE-6099 Global Search Enhancements (#2305)

* IDEMPIERE-6099 Global Search Enhancements

* IDEMPIERE-6099 Global Search Enhancements
This commit is contained in:
hengsin 2024-04-10 20:21:24 +08:00 committed by Carlos Ruiz
parent 0d0c33d197
commit 9799cebac8
5 changed files with 184 additions and 21 deletions

View File

@ -21,6 +21,7 @@ import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import org.adempiere.webui.LayoutUtils;
import org.adempiere.webui.component.Label; import org.adempiere.webui.component.Label;
import org.adempiere.webui.util.ZKUpdateUtil; import org.adempiere.webui.util.ZKUpdateUtil;
import org.compiere.model.I_AD_SearchDefinition; 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.DB;
import org.compiere.util.DisplayType; import org.compiere.util.DisplayType;
import org.compiere.util.Env; import org.compiere.util.Env;
import org.compiere.util.Msg;
import org.compiere.util.Util; import org.compiere.util.Util;
import org.zkoss.zk.ui.Component; import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.event.Event; import org.zkoss.zk.ui.event.Event;
@ -53,6 +55,8 @@ import org.zkoss.zul.Vlayout;
*/ */
public class DocumentSearchController implements EventListener<Event>{ public class DocumentSearchController implements EventListener<Event>{
/** 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} **/ /** {@link A} component attribute to hold reference to corresponding {@link #SEARCH_RESULT} **/
private static final String SEARCH_RESULT = "search.result"; private static final String SEARCH_RESULT = "search.result";
/** onSearchDocuments event **/ /** onSearchDocuments event **/
@ -64,6 +68,8 @@ public class DocumentSearchController implements EventListener<Event>{
private ArrayList<SearchResult> list; private ArrayList<SearchResult> list;
/** Current selected index of {@link #list} **/ /** Current selected index of {@link #list} **/
private int selected = -1; private int selected = -1;
/** True when showing transaction code available */
private boolean showingGuide = false;
/** /**
* default constructor * default constructor
@ -92,7 +98,12 @@ public class DocumentSearchController implements EventListener<Event>{
* @param value * @param value
*/ */
public void search(String 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); Events.echoEvent(ON_SEARCH_DOCUMENTS_EVENT, layout, value);
} }
@ -103,12 +114,31 @@ public class DocumentSearchController implements EventListener<Event>{
*/ */
private void onSearchDocuments(String searchString) { private void onSearchDocuments(String searchString) {
list = new ArrayList<SearchResult>(); list = new ArrayList<SearchResult>();
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<MSearchDefinition> 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; return;
} }
showingGuide = false;
// Search and show results
List<SearchResult> list = doSearch(searchString); List<SearchResult> 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<SearchResult>() { Collections.sort(list, new Comparator<SearchResult>() {
@Override @Override
public int compare(SearchResult o1, SearchResult o2) { public int compare(SearchResult o1, SearchResult o2) {
@ -118,20 +148,47 @@ public class DocumentSearchController implements EventListener<Event>{
return r; 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; String windowName = null;
for(SearchResult result : list) { for(SearchResult result : list) {
if (windowName == null || !windowName.equals(result.getWindowName())) { if (windowName == null || !windowName.equals(result.getWindowName())) {
windowName = result.getWindowName(); windowName = result.getWindowName();
Label label = new Label(windowName); Label label = new Label(windowName);
label.setStyle("padding: 3px; font-weight: bold; display: inline-block;"); LayoutUtils.addSclass("window-name", label);
layout.appendChild(label); layout.appendChild(label);
} }
A a = new A(); A a = new A();
a.setAttribute(SEARCH_RESULT, result); a.setAttribute(SEARCH_RESULT, result);
a.setLabel(result.getLabel());
layout.appendChild(a); layout.appendChild(a);
a.setStyle("padding-left: 3px; display: inline-block;"); LayoutUtils.addSclass("search-result", a);
a.addEventListener(Events.ON_CLICK, this); 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(); layout.invalidate();
} }
@ -146,7 +203,23 @@ public class DocumentSearchController implements EventListener<Event>{
final MRole role = MRole.get(Env.getCtx(), Env.getAD_Role_ID(Env.getCtx()), Env.getAD_User_ID(Env.getCtx()), true); final MRole role = MRole.get(Env.getCtx(), Env.getAD_Role_ID(Env.getCtx()), Env.getAD_User_ID(Env.getCtx()), true);
selected = -1; 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<MSearchDefinition> definitions = query.setOnlyActiveRecords(true).list(); List<MSearchDefinition> definitions = query.setOnlyActiveRecords(true).list();
for(MSearchDefinition msd : definitions) { for(MSearchDefinition msd : definitions) {
MTable table = new MTable(Env.getCtx(), msd.getAD_Table_ID(), null); MTable table = new MTable(Env.getCtx(), msd.getAD_Table_ID(), null);
@ -270,7 +343,15 @@ public class DocumentSearchController implements EventListener<Event>{
list.add(result); list.add(result);
} }
} catch (SQLException e) { } 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 { } finally {
DB.close(rs, pstmt); DB.close(rs, pstmt);
} }

View File

@ -100,9 +100,13 @@ public class GlobalSearch extends Div implements EventListener<Event> {
bandbox.addEventListener(Events.ON_CHANGE, this); bandbox.addEventListener(Events.ON_CHANGE, this);
bandbox.setCtrlKeys("#up#down"); bandbox.setCtrlKeys("#up#down");
bandbox.addEventListener(Events.ON_CTRL_KEY, this); bandbox.addEventListener(Events.ON_CTRL_KEY, this);
bandbox.addEventListener(Events.ON_FOCUS, e -> {
if (!bandbox.isOpen())
bandbox.setOpen(true);
});
Bandpopup popup = new Bandpopup(); Bandpopup popup = new Bandpopup();
ZKUpdateUtil.setWindowHeightX(popup, ClientInfo.get().desktopHeight-50); ZKUpdateUtil.setWindowHeightX(popup, ClientInfo.get().desktopHeight-100);
bandbox.appendChild(popup); bandbox.appendChild(popup);
tabbox = new Tabbox(); tabbox = new Tabbox();
@ -139,15 +143,18 @@ public class GlobalSearch extends Div implements EventListener<Event> {
@Override @Override
public void onEvent(Event event) throws Exception { public void onEvent(Event event) throws Exception {
if (Events.ON_CHANGING.equals(event.getName())) { 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; 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); bandbox.setAttribute(LAST_ONCHANGING_ATTR, value);
Events.postEvent(ON_SEARCH_EVENT, this, value); Events.postEvent(ON_SEARCH_EVENT, this, value);
} else if (Events.ON_CHANGE.equals(event.getName())) { } else if (Events.ON_CHANGE.equals(event.getName())) {
bandbox.removeAttribute(LAST_ONCHANGING_ATTR); bandbox.removeAttribute(LAST_ONCHANGING_ATTR);
} else if (Events.ON_CTRL_KEY.equals(event.getName())) { } 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; KeyEvent ke = (KeyEvent) event;
if (ke.getKeyCode() == KeyEvent.UP) { if (ke.getKeyCode() == KeyEvent.UP) {
if (bandbox.getFirstChild().isVisible()) { if (bandbox.getFirstChild().isVisible()) {
@ -180,10 +187,12 @@ public class GlobalSearch extends Div implements EventListener<Event> {
} }
} else if (event.getName().equals(ON_SEARCH_EVENT)) { } else if (event.getName().equals(ON_SEARCH_EVENT)) {
String value = (String) event.getData(); String value = (String) event.getData();
if (tabbox.getSelectedIndex()==0) if (tabbox.getSelectedIndex()==0) {
menuController.setHighlightText(value);
menuController.search(value); menuController.search(value);
else } else {
docController.search(value); docController.search(value);
}
bandbox.focus(); bandbox.focus();
} else if (event.getName().equals(ON_CREATE_ECHO_EVENT)) { } else if (event.getName().equals(ON_CREATE_ECHO_EVENT)) {
//setup client side listener for enter key //setup client side listener for enter key
@ -247,4 +256,20 @@ public class GlobalSearch extends Div implements EventListener<Event> {
public void onClientInfo() { public void onClientInfo() {
ZKUpdateUtil.setWindowHeightX(bandbox.getDropdown(), ClientInfo.get().desktopHeight-50); 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);
}
} }

View File

@ -17,6 +17,8 @@ import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; 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.ListHead;
import org.adempiere.webui.component.ListItem; import org.adempiere.webui.component.ListItem;
import org.adempiere.webui.component.Listbox; import org.adempiere.webui.component.Listbox;
@ -96,6 +98,8 @@ public class MenuSearchController implements EventListener<Event>{
/** true when controller is handling event from Star/Favourite button **/ /** true when controller is handling event from Star/Favourite button **/
private boolean inStarEvent; private boolean inStarEvent;
private String highlightText = null;
/** Event post from {@link #selectTreeitem(Object, Boolean)} **/ /** Event post from {@link #selectTreeitem(Object, Boolean)} **/
private static final String ON_POST_SELECT_TREEITEM_EVENT = "onPostSelectTreeitem"; private static final String ON_POST_SELECT_TREEITEM_EVENT = "onPostSelectTreeitem";
@ -604,6 +608,14 @@ public class MenuSearchController implements EventListener<Event>{
return false; return false;
} }
/**
* Set text to highlight
* @param s
*/
public void setHighlightText(String s) {
highlightText = s;
}
/** /**
* {@link ListitemRenderer} for {@link #listbox} * {@link ListitemRenderer} for {@link #listbox}
*/ */
@ -634,6 +646,33 @@ public class MenuSearchController implements EventListener<Event>{
cell.setImage(null); cell.setImage(null);
cell.setIconSclass(data.getImage()); 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); item.appendChild(cell);
cell.setTooltip(data.getDescription()); cell.setTooltip(data.getDescription());
item.setValue(data); item.setValue(data);

View File

@ -95,7 +95,7 @@ public class HeaderPanel extends Panel implements EventListener<Event>
btnMenu = (LabelImageElement) getFellow("menuButton"); btnMenu = (LabelImageElement) getFellow("menuButton");
btnMenu.setIconSclass("z-icon-sitemap"); 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); btnMenu.addEventListener(Events.ON_CLICK, this);
if (ClientInfo.isMobile()) { if (ClientInfo.isMobile()) {
LayoutUtils.addSclass("mobile", this); LayoutUtils.addSclass("mobile", this);
@ -131,6 +131,8 @@ public class HeaderPanel extends Panel implements EventListener<Event>
stub.getParent().insertBefore(globalSearch, stub); stub.getParent().insertBefore(globalSearch, stub);
stub.detach(); stub.detach();
globalSearch.setId("menuLookup"); globalSearch.setId("menuLookup");
globalSearch.setPlaceHolderText("Alt+G");
globalSearch.setTooltipText("Alt+G");
} }
@Override @Override
@ -155,15 +157,18 @@ public class HeaderPanel extends Panel implements EventListener<Event>
onCreate(); onCreate();
}else if (event instanceof KeyEvent) }else if (event instanceof KeyEvent)
{ {
//alt+m for the menu
KeyEvent ke = (KeyEvent) event; KeyEvent ke = (KeyEvent) event;
if (ke.getKeyCode() == 77) if (ke.getKeyCode() == 77) // alt+m for the menu
{ {
popMenu.open(btnMenu, "after_start"); popMenu.open(btnMenu, "after_start");
popMenu.setFocus(true); popMenu.setFocus(true);
}else if (ke.getKeyCode() == 27) { }
else if (ke.getKeyCode() == 27) // esc to close menu
{
popMenu.close(); popMenu.close();
}else if (ke.getKeyCode() == 71) { }
else if (ke.getKeyCode() == 71) // alt+g for the search
{
globalSearch.setFocus(true); globalSearch.setFocus(true);
} }
} else if(event.getName().equals(ZoomEvent.EVENT_NAME)) { } else if(event.getName().equals(ZoomEvent.EVENT_NAME)) {

View File

@ -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-"] { .menu-href [class^="z-icon-"] {
font-size: larger; font-size: larger;
color: #333; color: #333;