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.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;
@ -53,6 +55,8 @@ import org.zkoss.zul.Vlayout;
*/
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} **/
private static final String SEARCH_RESULT = "search.result";
/** onSearchDocuments event **/
@ -64,6 +68,8 @@ public class DocumentSearchController implements EventListener<Event>{
private ArrayList<SearchResult> 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<Event>{
* @param value
*/
public void search(String value) {
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<Event>{
*/
private void onSearchDocuments(String searchString) {
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;
}
showingGuide = false;
// Search and show results
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>() {
@Override
public int compare(SearchResult o1, SearchResult o2) {
@ -118,20 +148,47 @@ public class DocumentSearchController implements EventListener<Event>{
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<Event>{
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<MSearchDefinition> 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<Event>{
list.add(result);
}
} catch (SQLException e) {
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);
}

View File

@ -100,9 +100,13 @@ public class GlobalSearch extends Div implements EventListener<Event> {
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<Event> {
@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();
// 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<Event> {
}
} 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<Event> {
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);
}
}

View File

@ -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;
@ -96,6 +98,8 @@ public class MenuSearchController implements EventListener<Event>{
/** 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<Event>{
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<Event>{
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);

View File

@ -95,7 +95,7 @@ public class HeaderPanel extends Panel implements EventListener<Event>
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<Event>
stub.getParent().insertBefore(globalSearch, stub);
stub.detach();
globalSearch.setId("menuLookup");
globalSearch.setPlaceHolderText("Alt+G");
globalSearch.setTooltipText("Alt+G");
}
@Override
@ -155,15 +157,18 @@ public class HeaderPanel extends Panel implements EventListener<Event>
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)) {

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-"] {
font-size: larger;
color: #333;