IDEMPIERE-6105 Implement recently access menu items (#2311)

This commit is contained in:
hengsin 2024-04-20 10:55:41 +08:00 committed by Carlos Ruiz
parent e4e27f4ee5
commit 0eabdd9d08
7 changed files with 196 additions and 78 deletions

View File

@ -219,6 +219,7 @@ public class SystemIDs
public final static int WINDOW_LOT = 257; public final static int WINDOW_LOT = 257;
public final static int WINDOW_MATERIAL_RECEIPT = 184; public final static int WINDOW_MATERIAL_RECEIPT = 184;
public final static int WINDOW_MATERIALTRANSACTIONS_INDIRECTUSER = 223; 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_MY_REQUESTS = 237;
public final static int WINDOW_NOTICE = 193; public final static int WINDOW_NOTICE = 193;
public final static int WINDOW_PAYMENTS_INTO_BATCH = 200031; public final static int WINDOW_PAYMENTS_INTO_BATCH = 200031;

View File

@ -101,8 +101,9 @@ public class GlobalSearch extends Div implements EventListener<Event> {
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 -> { 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(); Bandpopup popup = new Bandpopup();
@ -224,6 +225,8 @@ public class GlobalSearch extends Div implements EventListener<Event> {
} }
} }
} else if (event.getName().equals(Events.ON_SELECT)) { } else if (event.getName().equals(Events.ON_SELECT)) {
if (tabbox.getSelectedIndex() == 0)
menuController.updateRecentItems();
String value = (String) bandbox.getAttribute(LAST_ONCHANGING_ATTR); String value = (String) bandbox.getAttribute(LAST_ONCHANGING_ATTR);
if (value == null) { if (value == null) {
value = bandbox.getValue(); value = bandbox.getValue();
@ -254,7 +257,7 @@ public class GlobalSearch extends Div implements EventListener<Event> {
* Handle client info event from browser. * Handle client info event from browser.
*/ */
public void onClientInfo() { public void onClientInfo() {
ZKUpdateUtil.setWindowHeightX(bandbox.getDropdown(), ClientInfo.get().desktopHeight-50); ZKUpdateUtil.setWindowHeightX(bandbox.getDropdown(), ClientInfo.get().desktopHeight-100);
} }
/** /**

View File

@ -31,8 +31,11 @@ import org.adempiere.webui.util.TreeNodeAction;
import org.adempiere.webui.util.TreeUtils; import org.adempiere.webui.util.TreeUtils;
import org.adempiere.webui.util.ZKUpdateUtil; import org.adempiere.webui.util.ZKUpdateUtil;
import org.compiere.model.MMenu; import org.compiere.model.MMenu;
import org.compiere.model.MPreference;
import org.compiere.model.MToolBarButtonRestrict; import org.compiere.model.MToolBarButtonRestrict;
import org.compiere.model.MTreeNode; import org.compiere.model.MTreeNode;
import org.compiere.model.Query;
import org.compiere.model.SystemIDs;
import org.compiere.util.Env; import org.compiere.util.Env;
import org.compiere.util.Msg; import org.compiere.util.Msg;
import org.compiere.util.Util; import org.compiere.util.Util;
@ -68,6 +71,9 @@ import org.zkoss.zul.impl.LabelImageElement;
*/ */
public class MenuSearchController implements EventListener<Event>{ public class MenuSearchController implements EventListener<Event>{
/** Initial number of menu items loaded into listbox */
private static final int INITIAL_LOADING_SIZE = 50;
/** Component attribute to hold reference of {@link MTreeNode} **/ /** Component attribute to hold reference of {@link MTreeNode} **/
public static final String M_TREE_NODE_ATTR = "MTreeNode"; public static final String M_TREE_NODE_ATTR = "MTreeNode";
@ -77,7 +83,7 @@ public class MenuSearchController implements EventListener<Event>{
private static final String Z_ICON_STAR = "z-icon-star"; private static final String Z_ICON_STAR = "z-icon-star";
/** Event echo from {@link #search(String)} to initiate search action **/ /** Event echo from {@link #search(String)} to initiate search action **/
private static final String ON_SEARCH_ECHO_EVENT = "onSearchEcho"; 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"; private static final String ON_LOAD_MORE_EVENT = "onLoadMore";
/** {@link Listitem} attribute to store the last timestamp of ON_CLICK or ON_SELECT event **/ /** {@link Listitem} attribute to store the last timestamp of ON_CLICK or ON_SELECT event **/
private static final String ONSELECT_TIMESTAMP_ATTR = "onselect.timestamp"; private static final String ONSELECT_TIMESTAMP_ATTR = "onselect.timestamp";
@ -100,20 +106,58 @@ public class MenuSearchController implements EventListener<Event>{
private String highlightText = null; private String highlightText = null;
/** List of recently access menu items (AD_Menu_ID) */
private List<String> recentMenuItemIds = new ArrayList<>();
/** 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";
/** /**
* @param tree * @param tree usually the tree instance from {@link}
*/ */
public MenuSearchController(Tree tree) { public MenuSearchController(Tree tree) {
this.tree = tree; this.tree = tree;
} }
/**
* Load recently access menu items
*/
private List<String> loadRecentItems() {
List<String> recents = new ArrayList<String>();
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<String> recents = loadRecentItems();
if (!recents.equals(recentMenuItemIds)) {
recentMenuItemIds = recents;
sortMenuItemModel();
moveRecentItems();
if (fullModel != null)
updateListboxModel(model);
}
}
/** /**
* Populate {@link #model} from {@link #tree} * Populate {@link #model} from {@link #tree}
*/ */
public void refreshModel() { public void refreshModel() {
recentMenuItemIds = loadRecentItems();
final List<MenuItem> list = new ArrayList<MenuItem>(); final List<MenuItem> list = new ArrayList<MenuItem>();
if (tree.getModel() == null) { if (tree.getModel() == null) {
TreeUtils.traverse(tree, new TreeItemAction() { TreeUtils.traverse(tree, new TreeItemAction() {
@ -130,8 +174,15 @@ public class MenuSearchController implements EventListener<Event>{
}); });
} }
model = new ListModelList<MenuItem>(list, true); model = new ListModelList<MenuItem>(list, true);
model.sort(new Comparator<MenuItem>() { sortMenuItemModel();
moveRecentItems();
}
/**
* Sort menu items model in alphabetical order
*/
private void sortMenuItemModel() {
model.sort(new Comparator<MenuItem>() {
@Override @Override
public int compare(MenuItem o1, MenuItem o2) { public int compare(MenuItem o1, MenuItem o2) {
return o1.getLabel().compareTo(o2.getLabel()); return o1.getLabel().compareTo(o2.getLabel());
@ -139,6 +190,35 @@ public class MenuSearchController implements EventListener<Event>{
}, true); }, true);
} }
/**
* Move the 7 most recently access menu items to the top of menu items model
*/
private void moveRecentItems() {
if (recentMenuItemIds.size() > 0) {
List<MenuItem> recents = new ArrayList<MenuItem>();
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 * Add treeNode to list
* @param list * @param list
@ -373,16 +453,16 @@ public class MenuSearchController implements EventListener<Event>{
/** /**
* Load {@link #fullModel} to {@link #listbox}. * 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() { private void loadMore() {
ListModel<MenuItem> listModel = listbox.getModel(); ListModel<MenuItem> listModel = listbox.getModel();
ListModelList<MenuItem> lml = (ListModelList<MenuItem>) listModel; ListModelList<MenuItem> lml = (ListModelList<MenuItem>) listModel;
lml.remove(lml.size()-1); lml.remove(lml.size()-1);
List<MenuItem> subList = fullModel.subList(50, fullModel.size()); List<MenuItem> subList = fullModel.subList(INITIAL_LOADING_SIZE, fullModel.size());
lml.addAll(subList); lml.addAll(subList);
fullModel = null; fullModel = null;
listbox.setSelectedIndex(50); listbox.setSelectedIndex(INITIAL_LOADING_SIZE);
Clients.scrollIntoView(listbox.getSelectedItem()); Clients.scrollIntoView(listbox.getSelectedItem());
} }
@ -430,8 +510,8 @@ public class MenuSearchController implements EventListener<Event>{
} }
/** /**
* Handle {@link #ON_POST_SELECT_TREEITEM_EVENT} event. * Handle {@link #ON_POST_SELECT_TREEITEM_EVENT} event.<br/>
* Post ON_CLICK event to link ({@link A} or {@link Treerow}). * Post ON_CLICK event to link ({@link A} or {@link Treerow}, handle in {@link AbstractMenuPanel}).
* @param newRecord * @param newRecord
*/ */
private void onPostSelectTreeitem(Boolean newRecord) { private void onPostSelectTreeitem(Boolean newRecord) {
@ -441,9 +521,10 @@ public class MenuSearchController implements EventListener<Event>{
event = new Event(Events.ON_CLICK, tree.getSelectedItem().getTreerow().getFirstChild().getFirstChild(), newRecord); event = new Event(Events.ON_CLICK, tree.getSelectedItem().getTreerow().getFirstChild().getFirstChild(), newRecord);
} }
else else
{ {
event = new Event(Events.ON_CLICK, tree.getSelectedItem().getTreerow(), newRecord); event = new Event(Events.ON_CLICK, tree.getSelectedItem().getTreerow(), newRecord);
} }
Events.postEvent(event); Events.postEvent(event);
} }
@ -474,15 +555,15 @@ public class MenuSearchController implements EventListener<Event>{
/** /**
* Update {@link #listbox} with newModel. * 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}. * User has to click the load more link (...) to load the rest of the items into {@link #listbox}.
* @param newModel * @param newModel
*/ */
private void updateListboxModel(ListModelList<MenuItem> newModel) { private void updateListboxModel(ListModelList<MenuItem> newModel) {
fullModel = null; fullModel = null;
if (newModel.size() > 50) { if (newModel.size() > INITIAL_LOADING_SIZE) {
List<MenuItem> list = newModel.getInnerList(); List<MenuItem> list = newModel.getInnerList();
List<MenuItem> subList = list.subList(0, 50); List<MenuItem> subList = list.subList(0, INITIAL_LOADING_SIZE);
fullModel = newModel; fullModel = newModel;
newModel = new ListModelList<MenuItem>(subList.toArray(new MenuItem[0])); newModel = new ListModelList<MenuItem>(subList.toArray(new MenuItem[0]));
MenuItem more = new MenuItem(); MenuItem more = new MenuItem();

View File

@ -15,19 +15,12 @@ import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.logging.Level; import java.util.logging.Level;
import org.adempiere.util.Callback;
import org.adempiere.webui.ClientInfo; 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.FavouriteController;
import org.adempiere.webui.desktop.IDesktop;
import org.adempiere.webui.exception.ApplicationException; import org.adempiere.webui.exception.ApplicationException;
import org.adempiere.webui.session.SessionManager; import org.adempiere.webui.session.SessionManager;
import org.adempiere.webui.theme.ThemeManager; import org.adempiere.webui.theme.ThemeManager;
import org.compiere.model.MMenu; import org.compiere.model.MMenu;
import org.compiere.model.MQuery;
import org.compiere.model.MTable;
import org.compiere.model.MToolBarButtonRestrict; import org.compiere.model.MToolBarButtonRestrict;
import org.compiere.model.MTreeNode; import org.compiere.model.MTreeNode;
import org.compiere.util.CLogger; import org.compiere.util.CLogger;
@ -331,27 +324,7 @@ public class FavoriteSimpleTreeModel extends SimpleTreeModel implements EventLis
{ {
try try
{ {
MMenu menu = (MMenu) MTable.get(Env.getCtx(), MMenu.Table_ID).getPO(menuID, null); SessionManager.getAppDesktop().onNewRecord(menuID);
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<ADWindow>() {
@Override
public void onCallback(ADWindow result)
{
if (result == null)
return;
result.getADWindowContent().onNew();
ADTabpanel adtabpanel = (ADTabpanel) result.getADWindowContent().getADTab().getSelectedTabpanel();
adtabpanel.focusToFirstEditor(false);
}
});
} }
catch (Exception e) catch (Exception e)
{ {

View File

@ -16,15 +16,24 @@ package org.adempiere.webui.desktop;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import org.adempiere.util.Callback;
import org.adempiere.webui.AdempiereWebUI; import org.adempiere.webui.AdempiereWebUI;
import org.adempiere.webui.ClientInfo; import org.adempiere.webui.ClientInfo;
import org.adempiere.webui.LayoutUtils; 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.component.Window;
import org.adempiere.webui.event.DialogEvents; import org.adempiere.webui.event.DialogEvents;
import org.adempiere.webui.exception.ApplicationException; import org.adempiere.webui.exception.ApplicationException;
import org.adempiere.webui.part.AbstractUIPart; import org.adempiere.webui.part.AbstractUIPart;
import org.adempiere.webui.session.SessionManager;
import org.compiere.model.MMenu; 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.CLogger;
import org.compiere.util.Env; import org.compiere.util.Env;
import org.zkoss.zk.ui.Component; import org.zkoss.zk.ui.Component;
@ -58,8 +67,8 @@ public abstract class AbstractDesktop extends AbstractUIPart implements IDesktop
/** /**
* Event listener for menu item selection.<br/> * Event listener for menu item selection.<br/>
* Identifies the action associated with the selected * Identifies the action associated with the selected menu item and acts accordingly.<br/>
* 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 * @param menuId Identifier for the selected menu item
* *
@ -110,8 +119,78 @@ public abstract class AbstractDesktop extends AbstractUIPart implements IDesktop
{ {
setPredefinedContextVariables(null); setPredefinedContextVariables(null);
} }
updateRecentMenuItem(menuId);
} }
/**
* Open AD window in new record mode.<br/>
* 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<ADWindow>() {
@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<String> itemList = new ArrayList<String>();
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} * @return {@link ClientInfo}
*/ */

View File

@ -49,11 +49,17 @@ public interface IDesktop extends UIPart {
public ClientInfo getClientInfo(); public ClientInfo getClientInfo();
/** /**
* * Launch menu item
* @param nodeId * @param nodeId
*/ */
public void onMenuSelected(int nodeId); public void onMenuSelected(int nodeId);
/**
* Launch AD Window in new record mode
* @param menuId
*/
public void onNewRecord(int menuId);
/** /**
* *
* @param window * @param window

View File

@ -20,18 +20,12 @@ import java.util.Collection;
import java.util.Enumeration; import java.util.Enumeration;
import java.util.Properties; 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.apps.MenuSearchController;
import org.adempiere.webui.desktop.AbstractDesktop;
import org.adempiere.webui.desktop.IDesktop;
import org.adempiere.webui.exception.ApplicationException; import org.adempiere.webui.exception.ApplicationException;
import org.adempiere.webui.session.SessionManager; import org.adempiere.webui.session.SessionManager;
import org.adempiere.webui.theme.ThemeManager; import org.adempiere.webui.theme.ThemeManager;
import org.adempiere.webui.util.ZKUpdateUtil; import org.adempiere.webui.util.ZKUpdateUtil;
import org.compiere.model.MMenu; import org.compiere.model.MMenu;
import org.compiere.model.MQuery;
import org.compiere.model.MToolBarButtonRestrict; import org.compiere.model.MToolBarButtonRestrict;
import org.compiere.model.MTree; import org.compiere.model.MTree;
import org.compiere.model.MTreeNode; import org.compiere.model.MTreeNode;
@ -291,7 +285,8 @@ public abstract class AbstractMenuPanel extends Panel implements EventListener<E
} }
/** /**
* Handle onClick and onOk event * Handle onClick and onOk event for menu tree item.<br/>
* The event from global search and application menu tree will be routed to here.
* @param comp * @param comp
* @param eventData * @param eventData
*/ */
@ -361,31 +356,11 @@ public abstract class AbstractMenuPanel extends Panel implements EventListener<E
private void onNewRecord(Treeitem selectedItem) { private void onNewRecord(Treeitem selectedItem) {
try try
{ {
if (getParent() instanceof Popup) {
((Popup)getParent()).close();
}
int menuId = Integer.parseInt((String)selectedItem.getValue()); int menuId = Integer.parseInt((String)selectedItem.getValue());
MMenu menu = new MMenu(Env.getCtx(), menuId, null); SessionManager.getAppDesktop().onNewRecord(menuId);
IDesktop desktop = SessionManager.getAppDesktop();
if (desktop instanceof AbstractDesktop)
((AbstractDesktop)desktop).setPredefinedContextVariables(menu.getPredefinedContextVariables());
MQuery query = new MQuery("");
query.addRestriction("1=2");
query.setRecordCount(0);
if (getParent() instanceof Popup) {
((Popup)getParent()).close();
}
SessionManager.getAppDesktop().openWindow(menu.getAD_Window_ID(), query, new Callback<ADWindow>() {
@Override
public void onCallback(ADWindow result) {
if(result == null)
return;
result.getADWindowContent().onNew();
ADTabpanel adtabpanel = (ADTabpanel) result.getADWindowContent().getADTab().getSelectedTabpanel();
adtabpanel.focusToFirstEditor(false);
}
});
} }
catch (Exception e) catch (Exception e)
{ {