From 1ae1aa040ed35b54afb68b37a7442d78cb994931 Mon Sep 17 00:00:00 2001 From: hengsin Date: Tue, 1 Mar 2022 16:05:36 +0800 Subject: [PATCH] IDEMPIERE-5202 Implement auto save of current tab (#1201) * IDEMPIERE-5202 Implement auto save of current tab * IDEMPIERE-5202 Implement auto save of current tab - Fix auto save trigger for every keystore on text field. * IDEMPIERE-5202 Implement auto save of current tab - Fix handling of the confirmation dialog for close of tab. - Focus tweak for child to parent tab navigation. - Restore focus if user abandon closing of tab. * IDEMPIERE-5202 Implement auto save of current tab - Fix infinite error loop when auto save fail with error --- .../oracle/202202232008_IDEMPIERE-5202.sql | 10 +++ .../202202232008_IDEMPIERE-5202.sql | 7 ++ .../src/org/compiere/model/GridTab.java | 9 +++ .../src/org/compiere/model/GridTable.java | 48 ++++++++++++++ .../src/org/compiere/model/MSysConfig.java | 1 + .../adwindow/AbstractADWindowContent.java | 66 +++++++++++++++++-- .../webui/adwindow/CompositeADTabbox.java | 1 + .../org/adempiere/webui/editor/WEditor.java | 8 +++ 8 files changed, 146 insertions(+), 4 deletions(-) create mode 100644 migration/iD10/oracle/202202232008_IDEMPIERE-5202.sql create mode 100644 migration/iD10/postgresql/202202232008_IDEMPIERE-5202.sql diff --git a/migration/iD10/oracle/202202232008_IDEMPIERE-5202.sql b/migration/iD10/oracle/202202232008_IDEMPIERE-5202.sql new file mode 100644 index 0000000000..cb67383991 --- /dev/null +++ b/migration/iD10/oracle/202202232008_IDEMPIERE-5202.sql @@ -0,0 +1,10 @@ +-- IDEMPIERE-5202 Implement auto save of current tab +SELECT register_migration_script('202202232008_IDEMPIERE-5202.sql') FROM dual; + +SET SQLBLANKLINES ON +SET DEFINE OFF + +-- Feb 23, 2022, 8:08:00 PM MYT +INSERT INTO AD_SysConfig (AD_SysConfig_ID,AD_Client_ID,AD_Org_ID,Created,Updated,CreatedBy,UpdatedBy,IsActive,Name,Value,Description,EntityType,ConfigurationLevel,AD_SysConfig_UU) VALUES (200193,0,0,TO_TIMESTAMP('2022-02-23 20:07:59','YYYY-MM-DD HH24:MI:SS'),TO_TIMESTAMP('2022-02-23 20:07:59','YYYY-MM-DD HH24:MI:SS'),100,100,'Y','ZK_AUTO_SAVE_CHANGES','N','Y/N - Define if the application will auto save changes of current tab','D','C','f7d6a7ee-7af4-465c-a9c5-21094250c736') +; + diff --git a/migration/iD10/postgresql/202202232008_IDEMPIERE-5202.sql b/migration/iD10/postgresql/202202232008_IDEMPIERE-5202.sql new file mode 100644 index 0000000000..05afa2ccf6 --- /dev/null +++ b/migration/iD10/postgresql/202202232008_IDEMPIERE-5202.sql @@ -0,0 +1,7 @@ +-- IDEMPIERE-5202 Implement auto save of current tab +SELECT register_migration_script('202202232008_IDEMPIERE-5202.sql') FROM dual; + +-- Feb 23, 2022, 8:08:00 PM MYT +INSERT INTO AD_SysConfig (AD_SysConfig_ID,AD_Client_ID,AD_Org_ID,Created,Updated,CreatedBy,UpdatedBy,IsActive,Name,Value,Description,EntityType,ConfigurationLevel,AD_SysConfig_UU) VALUES (200193,0,0,TO_TIMESTAMP('2022-02-23 20:07:59','YYYY-MM-DD HH24:MI:SS'),TO_TIMESTAMP('2022-02-23 20:07:59','YYYY-MM-DD HH24:MI:SS'),100,100,'Y','ZK_AUTO_SAVE_CHANGES','N','Y/N - Define if the application will auto save changes of current tab','D','C','f7d6a7ee-7af4-465c-a9c5-21094250c736') +; + diff --git a/org.adempiere.base/src/org/compiere/model/GridTab.java b/org.adempiere.base/src/org/compiere/model/GridTab.java index 1c95799f6b..5ebc4f9e69 100644 --- a/org.adempiere.base/src/org/compiere/model/GridTab.java +++ b/org.adempiere.base/src/org/compiere/model/GridTab.java @@ -1026,6 +1026,15 @@ public class GridTab implements DataStatusListener, Evaluatee, Serializable return false; } // dataSave + /** + * + * @return true if need save and all mandatory field has value + */ + public boolean isNeedSaveAndMandatoryFill() + { + return m_mTable.isNeedSaveAndMandatoryFill(); + } + // Validate if the current tab record has changed in database or any parent record // Return if there are changes public boolean hasChangedCurrentTabAndParents() { diff --git a/org.adempiere.base/src/org/compiere/model/GridTable.java b/org.adempiere.base/src/org/compiere/model/GridTable.java index 82bb27d6ad..216daf9a48 100644 --- a/org.adempiere.base/src/org/compiere/model/GridTable.java +++ b/org.adempiere.base/src/org/compiere/model/GridTable.java @@ -2536,6 +2536,54 @@ public class GridTable extends AbstractTableModel return sb.toString(); } // getMandatory + /** + * + * @return true if need save and all mandatory field has value + */ + public boolean isNeedSaveAndMandatoryFill() { + if (!m_open) + { + return false; + } + // no need - not changed - row not positioned - no Value changed + if (m_rowChanged == -1) + { + return false; + } + // Value not changed + if (m_rowData == null) + { + return false; + } + + if (m_readOnly) + { + return false; + } + + // row not positioned - no Value changed + if (m_rowChanged == -1) + { + if (m_newRow != -1) // new row and nothing changed - might be OK + m_rowChanged = m_newRow; + else + { + return false; + } + } + + // get updated row data + Object[] rowData = getDataAtRow(m_rowChanged); + + // Check Mandatory + String missingColumns = getMandatory(rowData); + if (missingColumns.length() != 0) { + return false; + } + + return true; + } + /*************************************************************************/ /** LOB Info */ diff --git a/org.adempiere.base/src/org/compiere/model/MSysConfig.java b/org.adempiere.base/src/org/compiere/model/MSysConfig.java index 037643172a..d0f43a925f 100644 --- a/org.adempiere.base/src/org/compiere/model/MSysConfig.java +++ b/org.adempiere.base/src/org/compiere/model/MSysConfig.java @@ -174,6 +174,7 @@ public class MSysConfig extends X_AD_SysConfig public static final String VALIDATE_MATCHING_TO_ORDERED_QTY = "VALIDATE_MATCHING_TO_ORDERED_QTY"; public static final String WEBUI_LOGOURL = "WEBUI_LOGOURL"; public static final String ZK_ADVANCE_FIND_FILTER_COLUMN_LIST = "ZK_ADVANCE_FIND_FILTER_COLUMN_LIST"; + public static final String ZK_AUTO_SAVE_CHANGES = "ZK_AUTO_SAVE_CHANGES"; public static final String ZK_BROWSER_ICON = "ZK_BROWSER_ICON"; public static final String ZK_BROWSER_TITLE = "ZK_BROWSER_TITLE"; public static final String ZK_BUTTON_STYLE = "ZK_BUTTON_STYLE"; diff --git a/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/adwindow/AbstractADWindowContent.java b/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/adwindow/AbstractADWindowContent.java index d273d22173..549c728fb0 100644 --- a/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/adwindow/AbstractADWindowContent.java +++ b/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/adwindow/AbstractADWindowContent.java @@ -211,8 +211,10 @@ public abstract class AbstractADWindowContent extends AbstractUIPart implements /** * Maintain no of quick form tabs open */ - ArrayList quickFormOpenTabs = new ArrayList (); + protected ArrayList quickFormOpenTabs = new ArrayList (); + protected Component lastFocusEditor = null; + /** * Constructor * @param ctx @@ -246,6 +248,7 @@ public abstract class AbstractADWindowContent extends AbstractUIPart implements comp.addEventListener(ON_DEFER_SET_DETAILPANE_SELECTION_EVENT, this); comp.addEventListener(ON_FOCUS_DEFER_EVENT, this); comp.setAttribute(ITabOnSelectHandler.ATTRIBUTE_KEY, this); + return comp; } @@ -946,6 +949,8 @@ public abstract class AbstractADWindowContent extends AbstractUIPart implements protected ADWindow adwindow; + protected boolean showingOnExitDialog; + /** * @see ToolbarListener#onLock() */ @@ -1142,11 +1147,22 @@ public abstract class AbstractADWindowContent extends AbstractUIPart implements /** * @param callback */ - public void onExit(Callback callback) + public synchronized void onExit(Callback callback) { if (isPendingChanges()) { - FDialog.ask(curWindowNo, null, "CloseUnSave?", callback); + showingOnExitDialog = true; + FDialog.ask(curWindowNo, null, "CloseUnSave?", b -> { + showingOnExitDialog = false; + callback.onCallback(b); + if (!b) + { + //restore focus + if (lastFocusEditor != null && lastFocusEditor instanceof HtmlBasedComponent && + lastFocusEditor.getPage() != null && LayoutUtils.isReallyVisible(lastFocusEditor)) + ((HtmlBasedComponent)lastFocusEditor).focus(); + } + }); } else { @@ -1795,6 +1811,10 @@ public abstract class AbstractADWindowContent extends AbstractUIPart implements adTabbox.getSelectedGridTab().isNew() || (adTabbox.getSelectedDetailADTabpanel() != null && adTabbox.getSelectedDetailADTabpanel().getGridTab().isNew())); + if (!e.isError() && Util.isEmpty(adInfo)) { + autoSaveChanges(e); + } + // // No Rows if (e.getTotalRows() == 0 && insertRecord && !detailTab && !tabPanel.getGridTab().isSortTab()) @@ -1907,6 +1927,35 @@ public abstract class AbstractADWindowContent extends AbstractUIPart implements } + private synchronized void autoSaveChanges(DataStatusEvent e) { + if (!e.isInitEdit() && toolbar.isSaveEnable() && MSysConfig.getBooleanValue(MSysConfig.ZK_AUTO_SAVE_CHANGES, false, Env.getAD_Client_ID(Env.getCtx()))) { + final IADTabpanel dirtyTabpanel = adTabbox.getDirtyADTabpanel(); + if (dirtyTabpanel != null && !dirtyTabpanel.getGridTab().isSortTab() + && Util.isEmpty(dirtyTabpanel.getGridTab().getCommitWarning(), true) + && Env.isAutoCommit(ctx, curWindowNo)) { + if (dirtyTabpanel.getGridTab().isNeedSaveAndMandatoryFill()) { + //sleep needed for onClose to show confirmation dialog + try { + Thread.sleep(200); + } catch (InterruptedException e2) { + } + if (!showingOnExitDialog) + Executions.schedule(getComponent().getDesktop(), e1 -> asyncAutoSave(), new Event("onAutoSave")); + } + } + } + } + + private synchronized void asyncAutoSave() { + //ensure still dirty and can save + if (toolbar.isSaveEnable() && !showingOnExitDialog) { + final IADTabpanel dirtyTabpanel = adTabbox.getDirtyADTabpanel(); + if (dirtyTabpanel != null && dirtyTabpanel.getGridTab().isNeedSaveAndMandatoryFill()) { + onSave(false, false, null); + } + } + } + /** * @return boolean */ @@ -2397,7 +2446,7 @@ public abstract class AbstractADWindowContent extends AbstractUIPart implements if (result) { WindowValidatorEvent event = new WindowValidatorEvent(adwindow, WindowValidatorEventType.AFTER_SAVE.getName()); WindowValidatorManager.getInstance().fireWindowValidatorEvent(event, callback); - } else { + } else if (callback != null) { callback.onCallback(result); } } @@ -3896,4 +3945,13 @@ public abstract class AbstractADWindowContent extends AbstractUIPart implements public GridWindow getGridWindow() { return gridWindow; } + + /** + * set component of last focus editor. + * Use in onClose/Exit to restore focus + * @param component + */ + public void setLastFocusEditor(Component component) { + lastFocusEditor = component; + } } diff --git a/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/adwindow/CompositeADTabbox.java b/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/adwindow/CompositeADTabbox.java index 1bd0bf4c6e..9e27253358 100644 --- a/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/adwindow/CompositeADTabbox.java +++ b/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/adwindow/CompositeADTabbox.java @@ -1094,6 +1094,7 @@ public class CompositeADTabbox extends AbstractADTabbox } if (adtab.getGridTab().getCurrentRow() != currentRow) adtab.getGridTab().setCurrentRow(currentRow, true); + Executions.schedule(getComponent().getDesktop(), e->((ADTabpanel)headerTab).focusToFirstEditor(), new Event("onFocusToHeaderTab")); break; } } diff --git a/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/editor/WEditor.java b/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/editor/WEditor.java index 1ac8c9c3cf..347ea1039f 100644 --- a/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/editor/WEditor.java +++ b/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/editor/WEditor.java @@ -26,6 +26,7 @@ import java.util.Properties; import org.adempiere.webui.AdempiereWebUI; import org.adempiere.webui.ClientInfo; import org.adempiere.webui.LayoutUtils; +import org.adempiere.webui.adwindow.ADWindow; import org.adempiere.webui.adwindow.IFieldEditorContainer; import org.adempiere.webui.component.Bandbox; import org.adempiere.webui.component.Button; @@ -332,6 +333,13 @@ public abstract class WEditor implements EventListener, PropertyChangeLis component.addEventListener(INIT_EDIT_EVENT, this); component.setAttribute("idempiere.editor", this); + + component.addEventListener(Events.ON_FOCUS, e -> { + ADWindow adwindow = ADWindow.findADWindow(component); + if (adwindow != null) { + adwindow.getADWindowContent().setLastFocusEditor(component); + } + }); } /**