From ab108c0437dc2cddd8950bd080828f35e5ee5496 Mon Sep 17 00:00:00 2001 From: git_admin Date: Mon, 27 Apr 2026 08:32:42 +0000 Subject: [PATCH] Tower: upload cx_web_refresh_from_backend 16.0.1.0.0 (via marketplace) --- .../views/form/form_controller_patch.esm.js | 181 ++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 addons/cx_web_refresh_from_backend/static/src/views/form/form_controller_patch.esm.js diff --git a/addons/cx_web_refresh_from_backend/static/src/views/form/form_controller_patch.esm.js b/addons/cx_web_refresh_from_backend/static/src/views/form/form_controller_patch.esm.js new file mode 100644 index 0000000..e33c4ae --- /dev/null +++ b/addons/cx_web_refresh_from_backend/static/src/views/form/form_controller_patch.esm.js @@ -0,0 +1,181 @@ +/** @odoo-module **/ + +import {FormController} from "@web/views/form/form_controller"; +import {patch} from "@web/core/utils/patch"; +import {useService} from "@web/core/utils/hooks"; +import {onWillUnmount} from "@odoo/owl"; +import {ConfirmationDialog} from "@web/core/confirmation_dialog/confirmation_dialog"; + +// Patch the standard FormController to react on bus notifications +patch(FormController.prototype, "cx_web_refresh_from_backend.FormController", { + setup() { + // Call original setup logic + this._super(...arguments); + + // Get core services used by this behavior + this.busService = useService("bus_service"); + this.actionService = useService("action"); + this.notificationService = useService("notification"); + + // Timestamp of last local save (used to avoid immediate auto-refresh) + this._lastLocalSave = null; + + // Bind the handler to keep reference for cleanup + this._boundBusHandler = this._onBusNotification.bind(this); + + // Subscribe to bus notifications + this.busService.addEventListener("notification", this._boundBusHandler); + + // Cleanup subscription on component unmount + onWillUnmount(() => { + if (this.busService && this._boundBusHandler) { + this.busService.removeEventListener( + "notification", + this._boundBusHandler + ); + } + }); + }, + + /** + * Handle bus notification for view refresh. + * Listens for notifications with type "web.refresh_view" and delegates + * processing to _handleViewRefresh. + * + * @param {Event} event - Bus notification event + */ + async _onBusNotification({detail: notifications}) { + // Check if component is still alive + if (!this.model || !this.model.root) { + return; + } + + for (const {payload, type} of notifications) { + if (type === "web.refresh_view") { + await this._handleViewRefresh(payload); + } + } + }, + + /** + * Handle view refresh notification. + * + * Only refreshes when: + * - model matches current form model + * - requested view types include "form" (if specified) + * - record id matches current record (if specified) + * + * @param {Object} notification - Notification payload + */ + async _handleViewRefresh(notification) { + const {model, view_types = [], rec_ids = []} = notification; + + // Check if the model matches current form model + if (this.props.resModel !== model) { + return; + } + + // Check if view_type matches (if specified in notification) + if (view_types.length > 0 && !view_types.includes("form")) { + return; + } + + // Check if record ID matches (if rec_ids is specified) + const currentResId = this.model && this.model.root && this.model.root.resId; + if (rec_ids.length > 0 && (!currentResId || !rec_ids.includes(currentResId))) { + return; + } + + // Skip refresh when form is in a dialog or when a wizard is on top of the stack. + // Refreshing in that context can leave wizard/confirmation dialogs stuck open + // (e.g. confirm="..." in wizard view). + if (this.env.inDialog) { + return; + } + const currentController = this.actionService.currentController; + const currentAction = currentController && currentController.action; + if (currentAction && currentAction.target === "new") { + return; + } + + await this.refreshForm(); + }, + + /** + * Refresh the form with actual data from server. + * + * For normal forms: + * - if record is clean: perform a soft_reload action + * - if record has unsaved changes: ask for confirmation, then reload + * + * For wizards (dialogs, target="new"): + * - reload only the current record without full action reload + * + * @returns {Promise} + */ + async refreshForm() { + // Do not refresh immediately after an explicit save (debounce window) + if (this._lastLocalSave && Date.now() - this._lastLocalSave < 1000) { + return; + } + + if (!this.model || !this.model.root) { + return; + } + + // Check if this form is opened as a wizard (dialog) + const currentController = this.actionService.currentController; + const action = currentController && currentController.action; + const isWizard = action && action.target === "new"; + + const record = this.model.root; + + if (!isWizard && record.isDirty) { + // Ask user whether to discard unsaved changes before refreshing + const confirmed = await new Promise((resolve) => { + this.dialogService.add(ConfirmationDialog, { + title: this.env._t("Form is being refreshed from backend"), + body: this.env._t("All unsaved changes will be lost! Continue?"), + confirm: () => resolve(true), + cancel: () => resolve(false), + confirmLabel: this.env._t("Continue"), + cancelLabel: this.env._t("Cancel"), + }); + }); + + if (!confirmed) { + return; + } + } + + try { + await record.load(); + } catch (error) { + const message = + (error && error.data && error.data.message) || + (error && error.message) || + String(error); + this.notificationService.add( + this.env._t("Could not reload form. ") + message, + {type: "danger"} + ); + return; + } + + // Update the view (only if component is still mounted) + if (this.model && this.model.root) { + this.render(true); + } + }, + + /** + * Override of save button handler. + * + * Stores timestamp of last local save to avoid immediate auto-refresh + * triggered by our own changes. + */ + async saveButtonClicked() { + this._lastLocalSave = Date.now(); + return await this._super(...arguments); + }, +});