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:
@@ -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;
|
||||
},
|
||||
});
|
||||
@@ -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});
|
||||
},
|
||||
});
|
||||
@@ -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});
|
||||
},
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user