Tower: upload cx_web_refresh_from_backend 18.0.1.0.0 (was 18.0.1.0.0, via marketplace)

This commit is contained in:
2026-05-03 18:55:11 +00:00
parent ed5f0d6535
commit e50acbac83
19 changed files with 1636 additions and 0 deletions

View File

@@ -0,0 +1,161 @@
/** @odoo-module */
import {FormController} from "@web/views/form/form_controller";
import {isResIdInRecIds} from "../utils/get_loaded_record_ids.esm";
import {onWillUnmount} from "@odoo/owl";
import {patch} from "@web/core/utils/patch";
import {useService} from "@web/core/utils/hooks";
import {_t} from "@web/core/l10n/translation";
patch(FormController.prototype, {
setup() {
super.setup(...arguments);
// Bus_service is async; useService("bus_service") breaks (SERVICES_METADATA).
this.busService = this.env.services.bus_service;
this.notificationService = useService("notification");
this._lastLocalSave = null;
this._isRefreshInFlight = false;
this._hasRefreshQueued = false;
this._boundBusHandler = this._onWebRefreshNotification.bind(this);
this.busService.subscribe("web.refresh_view", this._boundBusHandler);
onWillUnmount(() => {
if (this.busService && this._boundBusHandler) {
this.busService.unsubscribe("web.refresh_view", this._boundBusHandler);
}
});
},
/**
* Handle a web.refresh_view bus notification for this form.
* Called once per notification; coalesces concurrent refreshes via _queueRefresh.
*
* @param {Object} payload - Notification payload {model, view_types, rec_ids}
*/
async _onWebRefreshNotification(payload) {
if (!this.model || !this.model.root) {
return;
}
if (this._shouldRefreshView(payload)) {
await this._queueRefresh("refreshForm");
}
},
async _queueRefresh(methodName) {
if (this._isRefreshInFlight) {
this._hasRefreshQueued = true;
return;
}
this._isRefreshInFlight = true;
try {
do {
this._hasRefreshQueued = false;
await this[methodName]();
} while (this._hasRefreshQueued);
} finally {
this._isRefreshInFlight = false;
}
},
/**
* Check whether a refresh notification is relevant to this form.
*
* Returns true when all of the following hold:
* - model matches current form model
* - requested view types include "form" (or none specified)
* - record id matches current record (or none specified)
* - form is not inside a dialog / wizard
*
* @param {Object} payload - Notification payload
* @returns {Boolean}
*/
_shouldRefreshView(payload) {
const {model, view_types = [], rec_ids = []} = payload;
if (this.props.resModel !== model) {
return false;
}
if (view_types.length > 0 && !view_types.includes("form")) {
return false;
}
const currentResId = this.model && this.model.root && this.model.root.resId;
if (rec_ids.length > 0 && !isResIdInRecIds(currentResId, rec_ids)) {
return false;
}
// 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 false;
}
const currentController = this.actionService.currentController;
const currentAction = currentController && currentController.action;
if (currentAction && currentAction.target === "new") {
return false;
}
return true;
},
/**
* Refresh the form with actual data from server.
*
* Reloads without confirmation even when the record is dirty (client changes
* may be discarded). Dialog / wizard forms are filtered out in
* _shouldRefreshView().
*
* @returns {Promise<void>}
*/
async refreshForm() {
if (this._lastLocalSave && Date.now() - this._lastLocalSave < 2500) {
return;
}
if (!this.model || !this.model.root) {
return;
}
const record = this.model.root;
try {
await record.load();
} catch (error) {
this.notificationService.add(this._getRefreshErrorMessage(error), {
type: "danger",
});
return;
}
if (this.model && this.model.root) {
this.render(true);
}
},
_getRefreshErrorMessage(error) {
const message =
(error && error.data && error.data.message) ||
(error && error.message) ||
String(error);
return _t("Could not reload form. %(message)s", {message});
},
/**
* Override of save button handler.
*
* After a successful save, stores a timestamp to avoid immediate auto-refresh
* triggered by our own write (bus notification). Failed saves leave the
* timestamp unchanged so refresh suppression does not apply incorrectly.
*
* @param {Object} params - Save options
* @returns {Promise<Boolean|undefined>} Result of the core save (truthy when save succeeded)
*/
async saveButtonClicked(params) {
const result = await super.saveButtonClicked(params);
if (result) {
this._lastLocalSave = Date.now();
}
return result;
},
});

View File

@@ -0,0 +1,125 @@
/** @odoo-module */
import {
getLoadedRecordIds,
hasAnyLoadedIdInRecIds,
} from "../utils/get_loaded_record_ids.esm";
import {KanbanController} from "@web/views/kanban/kanban_controller";
import {onWillUnmount} from "@odoo/owl";
import {patch} from "@web/core/utils/patch";
import {useService} from "@web/core/utils/hooks";
import {_t} from "@web/core/l10n/translation";
patch(KanbanController.prototype, {
setup() {
super.setup(...arguments);
// Bus_service is async; useService("bus_service") breaks (SERVICES_METADATA).
this.busService = this.env.services.bus_service;
this.notificationService = useService("notification");
this._isRefreshInFlight = false;
this._hasRefreshQueued = false;
this._boundBusHandler = this._onWebRefreshNotification.bind(this);
this.busService.subscribe("web.refresh_view", this._boundBusHandler);
onWillUnmount(() => {
if (this.busService && this._boundBusHandler) {
this.busService.unsubscribe("web.refresh_view", this._boundBusHandler);
}
});
},
/**
* Handle a web.refresh_view bus notification for this kanban.
* Called once per notification; coalesces concurrent refreshes via _queueRefresh.
*
* @param {Object} payload - Notification payload {model, view_types, rec_ids}
*/
async _onWebRefreshNotification(payload) {
if (!this.model || !this.model.root) {
return;
}
if (this._shouldRefreshView(payload)) {
await this._queueRefresh("refreshList");
}
},
async _queueRefresh(methodName) {
if (this._isRefreshInFlight) {
this._hasRefreshQueued = true;
return;
}
this._isRefreshInFlight = true;
try {
do {
this._hasRefreshQueued = false;
await this[methodName]();
} while (this._hasRefreshQueued);
} finally {
this._isRefreshInFlight = false;
}
},
/**
* Check whether a refresh notification is relevant to this kanban.
*
* Returns true when all of the following hold:
* - model matches current kanban model
* - requested view types include "kanban" (or none specified)
* - at least one loaded record id is in rec_ids (or none specified)
*
* @param {Object} payload - Notification payload
* @returns {Boolean}
*/
_shouldRefreshView(payload) {
const {model, view_types = [], rec_ids = []} = payload;
if (this.props.resModel !== model) {
return false;
}
if (view_types.length > 0 && !view_types.includes("kanban")) {
return false;
}
if (rec_ids.length > 0) {
const loadedIds = getLoadedRecordIds(this.model.root);
if (!hasAnyLoadedIdInRecIds(loadedIds, rec_ids)) {
return false;
}
}
return true;
},
/**
* Refresh the kanban with actual data from server.
*
* @returns {Promise<void>}
*/
async refreshList() {
if (!this.model || !this.model.root) {
return;
}
const list = this.model.root;
try {
await list.load();
} catch (error) {
this.notificationService.add(this._getRefreshErrorMessage(error), {
type: "danger",
});
return;
}
if (this.model && this.model.root) {
this.render(true);
}
},
_getRefreshErrorMessage(error) {
const message =
(error && error.data && error.data.message) ||
(error && error.message) ||
String(error);
return _t("Could not reload kanban. %(message)s", {message});
},
});

View File

@@ -0,0 +1,170 @@
/** @odoo-module */
import {
getLoadedRecordIds,
hasAnyLoadedIdInRecIds,
} from "../utils/get_loaded_record_ids.esm";
import {ConfirmationDialog} from "@web/core/confirmation_dialog/confirmation_dialog";
import {ListController} from "@web/views/list/list_controller";
import {onWillUnmount} from "@odoo/owl";
import {patch} from "@web/core/utils/patch";
import {useService} from "@web/core/utils/hooks";
import {_t} from "@web/core/l10n/translation";
patch(ListController.prototype, {
setup() {
super.setup(...arguments);
// Bus_service is async; useService("bus_service") breaks (SERVICES_METADATA).
this.busService = this.env.services.bus_service;
this.notificationService = useService("notification");
this._isRefreshInFlight = false;
this._hasRefreshQueued = false;
this._boundBusHandler = this._onWebRefreshNotification.bind(this);
this.busService.subscribe("web.refresh_view", this._boundBusHandler);
onWillUnmount(() => {
if (this.busService && this._boundBusHandler) {
this.busService.unsubscribe("web.refresh_view", this._boundBusHandler);
}
});
},
/**
* Handle a web.refresh_view bus notification for this list.
* Called once per notification; coalesces concurrent refreshes via _queueRefresh.
*
* @param {Object} payload - Notification payload {model, view_types, rec_ids}
*/
async _onWebRefreshNotification(payload) {
if (!this.model || !this.model.root) {
return;
}
if (this._shouldRefreshView(payload)) {
await this._queueRefresh("refreshList");
}
},
async _queueRefresh(methodName) {
if (this._isRefreshInFlight) {
this._hasRefreshQueued = true;
return;
}
this._isRefreshInFlight = true;
try {
do {
this._hasRefreshQueued = false;
await this[methodName]();
} while (this._hasRefreshQueued);
} finally {
this._isRefreshInFlight = false;
}
},
/**
* Check whether a refresh notification is relevant to this list.
*
* Returns true when all of the following hold:
* - model matches current list model
* - requested view types include "list" or "tree" (or none specified)
* - at least one loaded record id is in rec_ids (or none specified)
*
* @param {Object} payload - Notification payload
* @returns {Boolean}
*/
_shouldRefreshView(payload) {
const {model, view_types = [], rec_ids = []} = payload;
if (this.props.resModel !== model) {
return false;
}
if (
view_types.length > 0 &&
!view_types.includes("list") &&
!view_types.includes("tree")
) {
return false;
}
if (rec_ids.length > 0) {
const loadedIds = getLoadedRecordIds(this.model.root);
if (!hasAnyLoadedIdInRecIds(loadedIds, rec_ids)) {
return false;
}
}
return true;
},
/**
* Refresh the list with actual data from server.
* If there is an edited record, asks the user to save or cancel.
*
* @returns {Promise<void>}
*/
async refreshList() {
if (!this.model || !this.model.root) {
return;
}
const list = this.model.root;
if (list.editedRecord) {
const confirmed = await this._confirmListRefresh();
if (!confirmed) {
// User declined: drop coalesced refreshes queued during the dialog.
this._hasRefreshQueued = false;
return;
}
try {
await list.editedRecord.save();
} catch (error) {
this.notificationService.add(this._getSaveErrorMessage(error), {
type: "danger",
});
return;
}
}
try {
await list.load();
} catch (error) {
this.notificationService.add(this._getReloadErrorMessage(error), {
type: "danger",
});
return;
}
if (this.model && this.model.root) {
this.render(true);
}
},
async _confirmListRefresh() {
return await new Promise((resolve) => {
this.dialogService.add(ConfirmationDialog, {
title: _t("List is being refreshed from backend"),
body: _t("You have unsaved edits. Save them before refreshing?"),
confirm: () => resolve(true),
cancel: () => resolve(false),
confirmLabel: _t("Save & Refresh"),
cancelLabel: _t("Cancel"),
});
});
},
_getSaveErrorMessage(error) {
const message =
(error && error.data && error.data.message) ||
(error && error.message) ||
String(error);
return _t("Could not save record. %(message)s", {message});
},
_getReloadErrorMessage(error) {
const message =
(error && error.data && error.data.message) ||
(error && error.message) ||
String(error);
return _t("Could not reload list. %(message)s", {message});
},
});

View File

@@ -0,0 +1,55 @@
/** @odoo-module */
/**
* Get IDs of records currently loaded in list-like root models.
* Supports both plain and grouped datasets.
*
* @param {Object} root - View root model (list/kanban)
* @returns {Array<Number>}
*/
export function getLoadedRecordIds(root) {
if (root.isGrouped) {
const recordIds = [];
const collectIds = (groups) => {
for (const group of groups) {
if (group.list && group.list.records) {
recordIds.push(...group.list.records.map((record) => record.resId));
}
if (group.groups) {
collectIds(group.groups);
}
}
};
collectIds(root.groups);
return recordIds;
}
return root.records.map((record) => record.resId);
}
/**
* Whether any loaded record id is present in the notification id list.
* Uses a Set for O(n + m) membership checks instead of O(n * m) with includes.
*
* @param {Number[]} loadedIds - IDs currently visible in the view
* @param {Number[]} rec_ids - IDs from the bus payload
* @returns {Boolean}
*/
export function hasAnyLoadedIdInRecIds(loadedIds, rec_ids) {
const recIdSet = new Set(rec_ids);
return loadedIds.some((id) => recIdSet.has(id));
}
/**
* Whether a single record id is in the notification id list.
* Uses a Set for O(m) build + O(1) lookup vs repeated includes.
*
* @param {Number|undefined|false} resId - Current record id (e.g. form root)
* @param {Number[]} rec_ids - IDs from the bus payload
* @returns {Boolean}
*/
export function isResIdInRecIds(resId, rec_ids) {
if (!resId) {
return false;
}
return new Set(rec_ids).has(resId);
}