diff --git a/org.adempiere.base/src/org/compiere/model/SystemIDs.java b/org.adempiere.base/src/org/compiere/model/SystemIDs.java index e4270ae248..ab75753e01 100644 --- a/org.adempiere.base/src/org/compiere/model/SystemIDs.java +++ b/org.adempiere.base/src/org/compiere/model/SystemIDs.java @@ -219,6 +219,7 @@ public class SystemIDs public final static int WINDOW_LOT = 257; public final static int WINDOW_MATERIAL_RECEIPT = 184; public final static int WINDOW_MATERIALTRANSACTIONS_INDIRECTUSER = 223; + public final static int WINDOW_MENU = 105; public final static int WINDOW_MY_REQUESTS = 237; public final static int WINDOW_NOTICE = 193; public final static int WINDOW_PAYMENTS_INTO_BATCH = 200031; 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 8876e6ce6d..ff3a5e896a 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 @@ -101,8 +101,9 @@ public class GlobalSearch extends Div implements EventListener { bandbox.setCtrlKeys("#up#down"); bandbox.addEventListener(Events.ON_CTRL_KEY, this); bandbox.addEventListener(Events.ON_FOCUS, e -> { - if (!bandbox.isOpen()) - bandbox.setOpen(true); + bandbox.setOpen(true); + if (Util.isEmpty(bandbox.getValue(), true) && tabbox.getSelectedIndex() == 0) + menuController.updateRecentItems(); }); Bandpopup popup = new Bandpopup(); @@ -224,6 +225,8 @@ public class GlobalSearch extends Div implements EventListener { } } } else if (event.getName().equals(Events.ON_SELECT)) { + if (tabbox.getSelectedIndex() == 0) + menuController.updateRecentItems(); String value = (String) bandbox.getAttribute(LAST_ONCHANGING_ATTR); if (value == null) { value = bandbox.getValue(); @@ -254,7 +257,7 @@ public class GlobalSearch extends Div implements EventListener { * Handle client info event from browser. */ public void onClientInfo() { - ZKUpdateUtil.setWindowHeightX(bandbox.getDropdown(), ClientInfo.get().desktopHeight-50); + ZKUpdateUtil.setWindowHeightX(bandbox.getDropdown(), ClientInfo.get().desktopHeight-100); } /** 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 28530e2dbb..de4cf4e142 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 @@ -31,8 +31,11 @@ import org.adempiere.webui.util.TreeNodeAction; import org.adempiere.webui.util.TreeUtils; import org.adempiere.webui.util.ZKUpdateUtil; import org.compiere.model.MMenu; +import org.compiere.model.MPreference; import org.compiere.model.MToolBarButtonRestrict; import org.compiere.model.MTreeNode; +import org.compiere.model.Query; +import org.compiere.model.SystemIDs; import org.compiere.util.Env; import org.compiere.util.Msg; import org.compiere.util.Util; @@ -68,6 +71,9 @@ import org.zkoss.zul.impl.LabelImageElement; */ public class MenuSearchController implements EventListener{ + /** Initial number of menu items loaded into listbox */ + private static final int INITIAL_LOADING_SIZE = 50; + /** Component attribute to hold reference of {@link MTreeNode} **/ public static final String M_TREE_NODE_ATTR = "MTreeNode"; @@ -77,7 +83,7 @@ public class MenuSearchController implements EventListener{ private static final String Z_ICON_STAR = "z-icon-star"; /** Event echo from {@link #search(String)} to initiate search action **/ private static final String ON_SEARCH_ECHO_EVENT = "onSearchEcho"; - /** Event to load all menu items into {@link #listbox}. Default is to load the first 50 only. **/ + /** Event to load all menu items into {@link #listbox}. Default is to load the first {@link #INITIAL_LOADING_SIZE} only. **/ private static final String ON_LOAD_MORE_EVENT = "onLoadMore"; /** {@link Listitem} attribute to store the last timestamp of ON_CLICK or ON_SELECT event **/ private static final String ONSELECT_TIMESTAMP_ATTR = "onselect.timestamp"; @@ -100,20 +106,58 @@ public class MenuSearchController implements EventListener{ private String highlightText = null; + /** List of recently access menu items (AD_Menu_ID) */ + private List recentMenuItemIds = new ArrayList<>(); + /** Event post from {@link #selectTreeitem(Object, Boolean)} **/ private static final String ON_POST_SELECT_TREEITEM_EVENT = "onPostSelectTreeitem"; /** - * @param tree + * @param tree usually the tree instance from {@link} */ public MenuSearchController(Tree tree) { this.tree = tree; } + /** + * Load recently access menu items + */ + private List loadRecentItems() { + List recents = new ArrayList(); + int AD_User_ID = Env.getAD_User_ID(Env.getCtx()); + int AD_Role_ID = Env.getAD_Role_ID(Env.getCtx()); + int AD_Org_ID = 0; + String attribute = AD_Role_ID+"|RecentMenuItems"; + Query query = new Query(Env.getCtx(), MPreference.Table_Name, "PreferenceFor=? AND Attribute=? AND AD_Org_ID=? AND AD_User_ID=? AND AD_Window_ID=?", null); + MPreference preference = query.setClient_ID().setParameters("W", attribute, AD_Org_ID, AD_User_ID, SystemIDs.WINDOW_MENU).first(); + if (preference != null) { + String[] recentItems = preference.getValue().split("[,]"); + for (String recentItem : recentItems) { + recents.add(recentItem); + } + } + return recents; + } + + /** + * If there are changes in the recent menu items for user, reload and update menu items model + */ + public void updateRecentItems() { + List recents = loadRecentItems(); + if (!recents.equals(recentMenuItemIds)) { + recentMenuItemIds = recents; + sortMenuItemModel(); + moveRecentItems(); + if (fullModel != null) + updateListboxModel(model); + } + } + /** * Populate {@link #model} from {@link #tree} */ public void refreshModel() { + recentMenuItemIds = loadRecentItems(); final List list = new ArrayList(); if (tree.getModel() == null) { TreeUtils.traverse(tree, new TreeItemAction() { @@ -130,8 +174,15 @@ public class MenuSearchController implements EventListener{ }); } model = new ListModelList(list, true); - model.sort(new Comparator() { - + sortMenuItemModel(); + moveRecentItems(); + } + + /** + * Sort menu items model in alphabetical order + */ + private void sortMenuItemModel() { + model.sort(new Comparator() { @Override public int compare(MenuItem o1, MenuItem o2) { return o1.getLabel().compareTo(o2.getLabel()); @@ -139,6 +190,35 @@ public class MenuSearchController implements EventListener{ }, true); } + /** + * Move the 7 most recently access menu items to the top of menu items model + */ + private void moveRecentItems() { + if (recentMenuItemIds.size() > 0) { + List recents = new ArrayList(); + for(String id : recentMenuItemIds) { + for(int i = 0; i < model.getSize(); i++) { + if (model.get(i).getData() instanceof Treeitem ti) { + if (ti.getValue() instanceof String tis) { + if (tis.equals(id)) { + recents.add(model.get(i)); + break; + } + } + } + } + } + if (recents.size() > 0) { + for (MenuItem mi : recents) { + model.remove(mi); + } + for(int i = recents.size()-1; i >= 0; i--) { + model.add(0, recents.get(i)); + } + } + } + } + /** * Add treeNode to list * @param list @@ -373,16 +453,16 @@ public class MenuSearchController implements EventListener{ /** * Load {@link #fullModel} to {@link #listbox}. - * Only first 50 loaded to {@link #listbox} initially. + * Only first {@link #INITIAL_LOADING_SIZE} loaded to {@link #listbox} initially. */ private void loadMore() { ListModel listModel = listbox.getModel(); ListModelList lml = (ListModelList) listModel; lml.remove(lml.size()-1); - List subList = fullModel.subList(50, fullModel.size()); + List subList = fullModel.subList(INITIAL_LOADING_SIZE, fullModel.size()); lml.addAll(subList); fullModel = null; - listbox.setSelectedIndex(50); + listbox.setSelectedIndex(INITIAL_LOADING_SIZE); Clients.scrollIntoView(listbox.getSelectedItem()); } @@ -430,8 +510,8 @@ public class MenuSearchController implements EventListener{ } /** - * Handle {@link #ON_POST_SELECT_TREEITEM_EVENT} event. - * Post ON_CLICK event to link ({@link A} or {@link Treerow}). + * Handle {@link #ON_POST_SELECT_TREEITEM_EVENT} event.
+ * Post ON_CLICK event to link ({@link A} or {@link Treerow}, handle in {@link AbstractMenuPanel}). * @param newRecord */ private void onPostSelectTreeitem(Boolean newRecord) { @@ -441,9 +521,10 @@ public class MenuSearchController implements EventListener{ event = new Event(Events.ON_CLICK, tree.getSelectedItem().getTreerow().getFirstChild().getFirstChild(), newRecord); } else - { + { event = new Event(Events.ON_CLICK, tree.getSelectedItem().getTreerow(), newRecord); } + Events.postEvent(event); } @@ -474,15 +555,15 @@ public class MenuSearchController implements EventListener{ /** * Update {@link #listbox} with newModel. - * If newModel has > 50 items, only first 50 is loaded into {@link #listbox}. + * If newModel has > {@link #INITIAL_LOADING_SIZE} items, only first {@link #INITIAL_LOADING_SIZE} is loaded into {@link #listbox}. * User has to click the load more link (...) to load the rest of the items into {@link #listbox}. * @param newModel */ private void updateListboxModel(ListModelList newModel) { fullModel = null; - if (newModel.size() > 50) { + if (newModel.size() > INITIAL_LOADING_SIZE) { List list = newModel.getInnerList(); - List subList = list.subList(0, 50); + List subList = list.subList(0, INITIAL_LOADING_SIZE); fullModel = newModel; newModel = new ListModelList(subList.toArray(new MenuItem[0])); MenuItem more = new MenuItem(); diff --git a/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/component/FavoriteSimpleTreeModel.java b/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/component/FavoriteSimpleTreeModel.java index 680427b055..343ecdfac0 100644 --- a/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/component/FavoriteSimpleTreeModel.java +++ b/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/component/FavoriteSimpleTreeModel.java @@ -15,19 +15,12 @@ import java.util.List; import java.util.Objects; import java.util.logging.Level; -import org.adempiere.util.Callback; import org.adempiere.webui.ClientInfo; -import org.adempiere.webui.adwindow.ADTabpanel; -import org.adempiere.webui.adwindow.ADWindow; -import org.adempiere.webui.desktop.AbstractDesktop; import org.adempiere.webui.desktop.FavouriteController; -import org.adempiere.webui.desktop.IDesktop; import org.adempiere.webui.exception.ApplicationException; import org.adempiere.webui.session.SessionManager; import org.adempiere.webui.theme.ThemeManager; import org.compiere.model.MMenu; -import org.compiere.model.MQuery; -import org.compiere.model.MTable; import org.compiere.model.MToolBarButtonRestrict; import org.compiere.model.MTreeNode; import org.compiere.util.CLogger; @@ -331,27 +324,7 @@ public class FavoriteSimpleTreeModel extends SimpleTreeModel implements EventLis { try { - MMenu menu = (MMenu) MTable.get(Env.getCtx(), MMenu.Table_ID).getPO(menuID, null); - IDesktop desktop = SessionManager.getAppDesktop(); - if (desktop instanceof AbstractDesktop) - ((AbstractDesktop)desktop).setPredefinedContextVariables(menu.getPredefinedContextVariables()); - - MQuery query = new MQuery(""); - query.addRestriction("1=2"); - query.setRecordCount(0); - - SessionManager.getAppDesktop().openWindow(menu.getAD_Window_ID(), query, new Callback() { - @Override - public void onCallback(ADWindow result) - { - if (result == null) - return; - - result.getADWindowContent().onNew(); - ADTabpanel adtabpanel = (ADTabpanel) result.getADWindowContent().getADTab().getSelectedTabpanel(); - adtabpanel.focusToFirstEditor(false); - } - }); + SessionManager.getAppDesktop().onNewRecord(menuID); } catch (Exception e) { diff --git a/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/desktop/AbstractDesktop.java b/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/desktop/AbstractDesktop.java index dbaeb15377..2eb1244a81 100644 --- a/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/desktop/AbstractDesktop.java +++ b/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/desktop/AbstractDesktop.java @@ -16,15 +16,24 @@ package org.adempiere.webui.desktop; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; +import org.adempiere.util.Callback; import org.adempiere.webui.AdempiereWebUI; import org.adempiere.webui.ClientInfo; import org.adempiere.webui.LayoutUtils; +import org.adempiere.webui.adwindow.ADTabpanel; +import org.adempiere.webui.adwindow.ADWindow; import org.adempiere.webui.component.Window; import org.adempiere.webui.event.DialogEvents; import org.adempiere.webui.exception.ApplicationException; import org.adempiere.webui.part.AbstractUIPart; +import org.adempiere.webui.session.SessionManager; import org.compiere.model.MMenu; +import org.compiere.model.MPreference; +import org.compiere.model.MQuery; +import org.compiere.model.Query; +import org.compiere.model.SystemIDs; import org.compiere.util.CLogger; import org.compiere.util.Env; import org.zkoss.zk.ui.Component; @@ -58,8 +67,8 @@ public abstract class AbstractDesktop extends AbstractUIPart implements IDesktop /** * Event listener for menu item selection.
- * Identifies the action associated with the selected - * menu item and acts accordingly. + * Identifies the action associated with the selected menu item and acts accordingly.
+ * Event from favourite panel, global search and application menu tree will be routed here. * * @param menuId Identifier for the selected menu item * @@ -110,8 +119,78 @@ public abstract class AbstractDesktop extends AbstractUIPart implements IDesktop { setPredefinedContextVariables(null); } + updateRecentMenuItem(menuId); } + /** + * Open AD window in new record mode.
+ * Call by global search, application menu tree and favourite panel. + * @param menuId + */ + @Override + public void onNewRecord(int menuId) { + MMenu menu = new MMenu(Env.getCtx(), menuId, null); + setPredefinedContextVariables(menu.getPredefinedContextVariables()); + + MQuery query = new MQuery(""); + query.addRestriction("1=2"); + query.setRecordCount(0); + + SessionManager.getAppDesktop().openWindow(menu.getAD_Window_ID(), query, new Callback() { + @Override + public void onCallback(ADWindow result) { + if(result == null) + return; + + result.getADWindowContent().onNew(); + ADTabpanel adtabpanel = (ADTabpanel) result.getADWindowContent().getADTab().getSelectedTabpanel(); + adtabpanel.focusToFirstEditor(false); + } + }); + updateRecentMenuItem(menuId); + } + + /** + * Perform asynchronous update of recent menu items preference for user + * @param menuId + */ + protected void updateRecentMenuItem(int menuId) { + Runnable runnable = () -> { + int AD_User_ID = Env.getAD_User_ID(Env.getCtx()); + int AD_Role_ID = Env.getAD_Role_ID(Env.getCtx()); + int AD_Org_ID = 0; + String attribute = AD_Role_ID+"|RecentMenuItems"; + Query query = new Query(Env.getCtx(), MPreference.Table_Name, "PreferenceFor=? AND Attribute=? AND AD_Org_ID=? AND AD_User_ID=? AND AD_Window_ID=?", null); + MPreference preference = query.setClient_ID().setParameters("W", attribute, AD_Org_ID, AD_User_ID, SystemIDs.WINDOW_MENU).first(); + if (preference == null) { + preference = new MPreference(Env.getCtx(), 0, null); + preference.setAD_Org_ID(AD_Org_ID); + preference.setPreferenceFor("W"); + preference.setAttribute(attribute); + preference.setAD_User_ID(AD_User_ID); + preference.setValue(Integer.toString(menuId)); + preference.setAD_Window_ID(SystemIDs.WINDOW_MENU); + preference.saveEx(); + } else { + String recentItemValue = preference.getValue(); + List itemList = new ArrayList(); + String[] recentItemValues = recentItemValue.split("[,]"); + String menuIdValue = Integer.toString(menuId); + itemList.add(menuIdValue); + for (int i = 0; itemList.size() < 7 && i < recentItemValues.length; i++) { + if (!recentItemValues[i].equals(menuIdValue)) + itemList.add(recentItemValues[i]); + } + recentItemValue = itemList.stream().collect(Collectors.joining(",")); + preference.setValue(recentItemValue); + preference.saveEx(); + } + }; + Executions.schedule(getComponent().getDesktop(), e -> { + runnable.run(); + }, new Event("onUpdateRecentMenuItem")); + } + /** * @return {@link ClientInfo} */ diff --git a/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/desktop/IDesktop.java b/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/desktop/IDesktop.java index e31c4f38b3..8bb70feddb 100644 --- a/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/desktop/IDesktop.java +++ b/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/desktop/IDesktop.java @@ -49,11 +49,17 @@ public interface IDesktop extends UIPart { public ClientInfo getClientInfo(); /** - * + * Launch menu item * @param nodeId */ public void onMenuSelected(int nodeId); + /** + * Launch AD Window in new record mode + * @param menuId + */ + public void onNewRecord(int menuId); + /** * * @param window diff --git a/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/panel/AbstractMenuPanel.java b/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/panel/AbstractMenuPanel.java index 96e4ac5123..16bf448a69 100644 --- a/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/panel/AbstractMenuPanel.java +++ b/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/panel/AbstractMenuPanel.java @@ -20,18 +20,12 @@ import java.util.Collection; import java.util.Enumeration; import java.util.Properties; -import org.adempiere.util.Callback; -import org.adempiere.webui.adwindow.ADTabpanel; -import org.adempiere.webui.adwindow.ADWindow; import org.adempiere.webui.apps.MenuSearchController; -import org.adempiere.webui.desktop.AbstractDesktop; -import org.adempiere.webui.desktop.IDesktop; import org.adempiere.webui.exception.ApplicationException; import org.adempiere.webui.session.SessionManager; import org.adempiere.webui.theme.ThemeManager; import org.adempiere.webui.util.ZKUpdateUtil; import org.compiere.model.MMenu; -import org.compiere.model.MQuery; import org.compiere.model.MToolBarButtonRestrict; import org.compiere.model.MTree; import org.compiere.model.MTreeNode; @@ -291,7 +285,8 @@ public abstract class AbstractMenuPanel extends Panel implements EventListener + * The event from global search and application menu tree will be routed to here. * @param comp * @param eventData */ @@ -361,31 +356,11 @@ public abstract class AbstractMenuPanel extends Panel implements EventListener() { - @Override - public void onCallback(ADWindow result) { - if(result == null) - return; - - result.getADWindowContent().onNew(); - ADTabpanel adtabpanel = (ADTabpanel) result.getADWindowContent().getADTab().getSelectedTabpanel(); - adtabpanel.focusToFirstEditor(false); - } - }); + SessionManager.getAppDesktop().onNewRecord(menuId); } catch (Exception e) {