合并企业版代码(未测试,先提交到测试分支)

This commit is contained in:
qihao.gong@jikimo.com
2023-04-14 17:42:23 +08:00
parent 7a7b3d7126
commit d28525526a
1300 changed files with 513579 additions and 5426 deletions

View File

@@ -0,0 +1,11 @@
odoo.define('web_enterprise.apps', function (require) {
"use strict";
var Apps = require('web.Apps');
Apps.include({
// Do nothing on update count as needactions have been removed in enterprise
_on_update_count: function() {},
});
});

View File

@@ -0,0 +1,145 @@
odoo.define('web_enterprise.ControlPanel', function (require) {
"use strict";
const ControlPanel = require('web.ControlPanel');
const { device } = require('web.config');
const { patch } = require('web.utils');
const { onMounted, useExternalListener, useRef, useState, useEffect } = owl;
const STICKY_CLASS = 'o_mobile_sticky';
if (!device.isMobile) {
return;
}
/**
* Control panel: mobile layout
*
* This patch handles the scrolling behaviour of the control panel in a mobile
* environment: the panel sticks to the top of the window when scrolling into
* the view. It is revealed when scrolling up and hiding when scrolling down.
* The panel's position is reset to default when at the top of the view.
*/
patch(ControlPanel.prototype, 'web_enterprise.ControlPanel', {
setup() {
this._super();
this.controlPanelRef = useRef("controlPanel");
this.state = useState({
showSearchBar: false,
showMobileSearch: false,
showViewSwitcher: false,
});
this.onWindowClick = this._onWindowClick.bind(this);
this.onScrollThrottled = this._onScrollThrottled.bind(this);
useExternalListener(window, "click", this.onWindowClick);
useEffect(() => {
const scrollingEl = this._getScrollingElement();
scrollingEl.addEventListener("scroll", this.onScrollThrottled);
this.controlPanelRef.el.style.top = "0px";
return () => {
scrollingEl.removeEventListener("scroll", this.onScrollThrottled);
}
})
onMounted(() => {
this.oldScrollTop = 0;
this.lastScrollTop = 0;
this.initialScrollTop = this._getScrollingElement().scrollTop;
});
},
//---------------------------------------------------------------------
// Private
//---------------------------------------------------------------------
_getScrollingElement() {
return this.controlPanelRef.el.parentElement;
},
/**
* Get today's date (number).
* @private
* @returns {number}
*/
_getToday() {
return new Date().getDate();
},
/**
* Reset mobile search state
* @private
*/
_resetSearchState() {
Object.assign(this.state, {
showSearchBar: false,
showMobileSearch: false,
showViewSwitcher: false,
});
},
//---------------------------------------------------------------------
// Handlers
//---------------------------------------------------------------------
/**
* Show or hide the control panel on the top screen.
* The function is throttled to avoid refreshing the scroll position more
* often than necessary.
* @private
*/
_onScrollThrottled() {
if (!this.controlPanelRef.el || this.isScrolling) {
return;
}
this.isScrolling = true;
requestAnimationFrame(() => this.isScrolling = false);
const scrollTop = this._getScrollingElement().scrollTop;
const delta = Math.round(scrollTop - this.oldScrollTop);
if (scrollTop > this.initialScrollTop) {
// Beneath initial position => sticky display
this.controlPanelRef.el.classList.add(STICKY_CLASS);
this.lastScrollTop = delta < 0 ?
// Going up
Math.min(0, this.lastScrollTop - delta) :
// Going down | not moving
Math.max(-this.controlPanelRef.el.offsetHeight, -this.controlPanelRef.el.offsetTop - delta);
this.controlPanelRef.el.style.top = `${this.lastScrollTop}px`;
} else {
// Above initial position => standard display
this.controlPanelRef.el.classList.remove(STICKY_CLASS);
this.lastScrollTop = 0;
}
this.oldScrollTop = scrollTop;
},
/**
* Reset mobile search state on switch view.
* @private
*/
_onSwitchView() {
this._resetSearchState();
},
/**
* @private
* @param {MouseEvent} ev
*/
_onWindowClick(ev) {
if (
this.state.showViewSwitcher &&
!ev.target.closest('.o_cp_switch_buttons')
) {
this.state.showViewSwitcher = false;
}
},
});
patch(ControlPanel, 'web_enterprise.ControlPanel', {
template: 'web_enterprise._ControlPanel',
});
});

View File

@@ -0,0 +1,75 @@
odoo.define("web_enterprise.Dialog", function(require) {
"use strict";
const config = require("web.config");
if (!config.device.isMobile) {
return;
}
const Dialog = require("web.Dialog");
Dialog.include({
/**
* @override
*
* @param {Widget} parent
* @param {Array} options.headerButtons
*/
init(parent, { headerButtons }) {
this._super.apply(this, arguments);
this.headerButtons = headerButtons || [];
},
/**
* Renders the header as a "Top App Bar" layout :
* - navigation icon: right arrow that closes the dialog;
* - title: same as on desktop;
* - action buttons: optional actions related to the current screen.
* Only applied for fullscreen dialog.
*
* Also, get the current scroll position for mobile devices in order to
* maintain offset while dialog is closed.
*
* @override
*/
async willStart() {
const prom = await this._super.apply(this, arguments);
if (this.renderHeader && this.fullscreen) {
const $modalHeader = this.$modal.find(".modal-header");
$modalHeader.find("button.btn-close").remove();
const $navigationBtn = $("<button>", {
class: "btn fa fa-arrow-left",
"data-bs-dismiss": "modal",
"aria-label": "close",
});
$modalHeader.prepend($navigationBtn);
const $btnContainer = $("<div>");
this._setButtonsTo($btnContainer, this.headerButtons);
$modalHeader.append($btnContainer);
}
// need to get scrollPosition prior opening the dialog else body will scroll to
// top due to fixed position applied on it with help of 'modal-open' class.
this.scrollPosition = {
top: window.scrollY || document.documentElement.scrollTop,
left: window.scrollX || document.documentElement.scrollLeft,
};
return prom;
},
/**
* Scroll to original position while closing modal in mobile devices
*
* @override
*/
destroy() {
if (this.$modal && $('.modal[role="dialog"]').filter(":visible").length <= 1) {
// in case of multiple open dialogs, only reset scroll while closing the last one
// (it can be done only if there's no fixed position on body and thus by removing
// 'modal-open' class responsible for fixed position)
this.$modal.closest("body").removeClass("modal-open");
window.scrollTo(this.scrollPosition);
}
this._super(...arguments);
},
});
});

View File

@@ -0,0 +1,104 @@
odoo.define("web.SearchPanel.Small", function (require) {
"use strict";
const SearchPanel = require("web.searchPanel");
const { device } = require("web.config");
const { patch } = require('web.utils');
if (!device.isMobile) {
return;
}
//-------------------------------------------------------------------------
// Helpers
//-------------------------------------------------------------------------
const isFilter = (s) => s.type === "filter";
/**
* @param {Map} values
* @returns {Object[]}
*/
function nameOfCheckedValues(values) {
const names = [];
for (const [ , value] of values) {
if (value.checked) {
names.push(value.display_name);
}
}
return names;
}
patch(SearchPanel.prototype, "web_enterprise.SearchPanel.Mobile", {
setup() {
this._super(...arguments);
this.state.showMobileSearch = false;
},
//-----------------------------------------------------------------
// Private
//-----------------------------------------------------------------
/**
* Returns a formatted version of the active categories to populate
* the selection banner of the control panel summary.
* @private
* @returns {Object[]}
*/
_getCategorySelection() {
const activeCategories = this.model.get("sections",
(s) => s.type === "category" && s.activeValueId
);
const selection = [];
for (const category of activeCategories) {
const parentIds = this._getAncestorValueIds(
category,
category.activeValueId
);
const orderedCategoryNames = [
...parentIds,
category.activeValueId,
].map(
(valueId) => category.values.get(valueId).display_name
);
selection.push({
values: orderedCategoryNames,
icon: category.icon,
color: category.color,
});
}
return selection;
},
/**
* Returns a formatted version of the active filters to populate
* the selection banner of the control panel summary.
* @private
* @returns {Object[]}
*/
_getFilterSelection() {
const filters = this.model.get("sections", isFilter);
const selection = [];
for (const { groups, values, icon, color } of filters) {
let filterValues;
if (groups) {
filterValues = Object.keys(groups)
.map((groupId) =>
nameOfCheckedValues(groups[groupId].values)
)
.flat();
} else if (values) {
filterValues = nameOfCheckedValues(values);
}
if (filterValues.length) {
selection.push({ values: filterValues, icon, color });
}
}
return selection;
},
});
patch(SearchPanel, "web_enterprise.SearchPanel.Mobile", {
template: "web_enterprise.Legacy.SearchPanel.Mobile",
});
});

View File

@@ -0,0 +1,121 @@
odoo.define('web_mobile.barcode_fields', function (require) {
"use strict";
var field_registry = require('web.field_registry');
require('web._field_registry');
var relational_fields = require('web.relational_fields');
const { _t } = require('web.core');
const BarcodeScanner = require('@web/webclient/barcode/barcode_scanner');
/**
* Override the Many2One to open a dialog in mobile.
*/
var FieldMany2OneBarcode = relational_fields.FieldMany2One.extend({
template: "FieldMany2OneBarcode",
events: _.extend({}, relational_fields.FieldMany2One.prototype.events, {
'click .o_barcode': '_onBarcodeButtonClick',
}),
/**
* @override
*/
start: function () {
var result = this._super.apply(this, arguments);
this._startBarcode();
return result;
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* External button is visible
*
* @return {boolean}
* @private
*/
_isExternalButtonVisible: function () {
return this.$external_button.is(':visible');
},
/**
* Hide the search more option
*
* @param {Array} values
*/
_manageSearchMore(values) {
return values;
},
/**
* @override
* @private
*/
_renderEdit: function () {
this._super.apply(this, arguments);
// Hide button if a record is set or external button is visible
if (this.$barcode_button) {
this.$barcode_button.toggle(!this._isExternalButtonVisible());
}
},
/**
* Initialisation of barcode button
*
* @private
*/
_startBarcode: function () {
this.$barcode_button = this.$('.o_barcode');
// Hide button if a record is set
this.$barcode_button.toggle(!this.isSet());
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* On click on button
*
* @private
*/
async _onBarcodeButtonClick() {
const barcode = await BarcodeScanner.scanBarcode();
if (barcode) {
this._onBarcodeScanned(barcode);
if ('vibrate' in window.navigator) {
window.navigator.vibrate(100);
}
} else {
this.displayNotification({
type: 'warning',
message: 'Please, scan again !',
});
}
},
/**
* When barcode is scanned
*
* @param barcode
* @private
*/
async _onBarcodeScanned(barcode) {
const results = await this._search(barcode);
const records = results.filter(r => !!r.id);
if (records.length === 1) {
this._setValue({ id: records[0].id });
} else {
const dynamicFilters = [{
description: _.str.sprintf(_t('Quick search: %s'), barcode),
domain: [['id', 'in', records.map(r => r.id)]],
}];
this._searchCreatePopup("search", false, {}, dynamicFilters);
}
},
});
if (BarcodeScanner.isBarcodeScannerSupported()) {
field_registry.add('many2one_barcode', FieldMany2OneBarcode);
}
return FieldMany2OneBarcode;
});

View File

@@ -0,0 +1,114 @@
odoo.define('web_enterprise.BasicRenderer', function (require) {
"use strict";
const config = require('web.config');
if (!config.device.isMobile) {
return;
}
/**
* This file defines the MobileBasicRenderer, an extension of the BasicRenderer
* implementing some tweaks to improve the UX in mobile.
*/
const BasicRenderer = require('web.BasicRenderer');
BasicRenderer.include({
SHOW_AFTER_DELAY: 250,
/**
* @override
*/
init() {
this._super(...arguments);
this.showTimer = undefined;
this.tooltipNodes = [];
this._onTouchStartTooltipBind = this._onTouchStartTooltip.bind(this);
this._onTouchEndTooltipBind = this._onTouchEndTooltip.bind(this);
},
/**
* @override
*/
on_attach_callback() {
this._super(...arguments);
this._addListener();
},
/**
* @override
*/
on_detach_callback() {
this._removeListeners();
this._super(...arguments);
},
_addListener: function () {
this.tooltipNodes.forEach((nodeElement) => {
nodeElement.addEventListener('touchstart', this._onTouchStartTooltipBind);
nodeElement.addEventListener('touchend', this._onTouchEndTooltipBind);
nodeElement.classList.add('o_user_select_none');
});
},
/**
* Allow to change when the tooltip appears
*
* @override
*/
_addFieldTooltip: function (widget, $node) {
this._super(...arguments);
$node = $node.length ? $node : widget.$el;
const nodeElement = $node[0];
if (!this.tooltipNodes.some(node => node === nodeElement)) {
this.tooltipNodes.push(nodeElement);
}
},
/**
* @override
*/
_getTooltipOptions: function () {
return Object.assign({}, this._super(...arguments), {
trigger: 'manual',
});
},
/**
* @override
*/
_render: function () {
return this._super(...arguments).then(() => {
this._addListener();
});
},
_removeListeners: function () {
while (this.tooltipNodes.length) {
const node = this.tooltipNodes.shift();
node.removeEventListener('touchstart', this._onTouchStartTooltipBind);
node.removeEventListener('touchend', this._onTouchEndTooltipBind);
node.classList.remove('o_user_select_none');
}
},
/**
* @private
* @param {TouchEvent} event
*/
_onTouchEndTooltip: function (event) {
clearTimeout(this.showTimer);
const $node = $(event.target);
$node.tooltip('hide');
},
/**
* @private
* @param {TouchEvent} event
*/
_onTouchStartTooltip: function (event) {
// Exclude children element from this handler
if (!event.target.classList.contains('o_user_select_none')) {
return;
}
const $node = $(event.target);
clearTimeout(this.showTimer);
this.showTimer = setTimeout(() => {
$node.tooltip('show');
}, this.SHOW_AFTER_DELAY);
},
});
});

View File

@@ -0,0 +1,84 @@
odoo.define('web_enterprise.MobileFormRenderer', function (require) {
"use strict";
const config = require('web.config');
if (!config.device.isMobile) {
return;
}
/**
* This file defines the MobileFormRenderer, an extension of the FormRenderer
* implementing some tweaks to improve the UX in mobile.
*/
const core = require('web.core');
const FormRenderer = require('web.FormRenderer');
const qweb = core.qweb;
FormRenderer.include({
/**
* Reset the 'state' of the drop down
* @override
*/
updateState: function () {
this.isStatusbarButtonsDropdownOpen = undefined;
return this._super(...arguments);
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Show action drop-down if there are multiple visible buttons/widgets.
*
* @private
* @override
*/
_renderStatusbarButtons: function (buttons) {
// Removes 'o_invisible_modifier' buttons in addition to those that match this selector:
// .o_form_view.o_form_editable .oe_read_only {
// display: none !important;
// }
const $visibleButtons = buttons.filter(button => {
return !($(button).hasClass('o_invisible_modifier') || (this.mode === 'edit' && $(button).hasClass('oe_read_only')));
});
if ($visibleButtons.length > 1) {
const $statusbarButtonsDropdown = $(qweb.render('StatusbarButtonsDropdown', {
open: this.isStatusbarButtonsDropdownOpen,
}));
$statusbarButtonsDropdown.find('.btn-group').on('show.bs.dropdown', () => {
this.isStatusbarButtonsDropdownOpen = true;
});
$statusbarButtonsDropdown.find('.btn-group').on('hide.bs.dropdown', () => {
this.isStatusbarButtonsDropdownOpen = false;
});
const $dropdownMenu = $statusbarButtonsDropdown.find('.dropdown-menu');
buttons.forEach(button => {
const dropdownButton = $(button).addClass('dropdown-item');
return $dropdownMenu.append(dropdownButton);
});
return $statusbarButtonsDropdown;
}
buttons.forEach(button => $(button).removeClass('dropdown-item'));
return this._super.apply(this, arguments);
},
/**
* Update the UI statusbar button after all modifiers are updated.
*
* @override
* @private
*/
_updateAllModifiers: function () {
return this._super.apply(this, arguments).then(() => {
const $statusbarButtonsContainer = this.$('.o_statusbar_buttons');
const $statusbarButtons = $statusbarButtonsContainer.find('button.btn').toArray();
$statusbarButtonsContainer.replaceWith(this._renderStatusbarButtons($statusbarButtons));
});
},
});
});

View File

@@ -0,0 +1,40 @@
odoo.define('web_mobile.FormView', function (require) {
"use strict";
var config = require('web.config');
var FormView = require('web.FormView');
var QuickCreateFormView = require('web.QuickCreateFormView');
/**
* We don't want to see the keyboard after the opening of a form view.
* The keyboard takes a lot of space and the user doesn't have a global view
* on the form.
* Plus, some relational fields are overrided (Many2One for example) and the
* user have to click on it to set a value. On this kind of field, the autofocus
* doesn't make sense because you can't directly type on it.
* So, we have to disable the autofocus in mobile.
*/
FormView.include({
/**
* @override
*/
init: function () {
this._super.apply(this, arguments);
if (config.device.isMobile) {
this.controllerParams.disableAutofocus = true;
}
},
});
QuickCreateFormView.include({
/**
* @override
*/
init: function () {
this._super.apply(this, arguments);
this.controllerParams.disableAutofocus = false;
},
});
});

View File

@@ -0,0 +1,73 @@
odoo.define('web.KanbanTabsMobileMixin', function () {
"use strict";
const KanbanTabsMobileMixin = {
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Update the tabs positions
*
* @param tabs - All existing tabs in the kanban
* @param moveToIndex - The current Active tab in the index
* @param $tabsContainer - the jquery container of the tabs
* @private
*/
_computeTabPosition(tabs, moveToIndex, $tabsContainer) {
this._computeTabJustification(tabs, $tabsContainer);
this._computeTabScrollPosition(tabs, moveToIndex, $tabsContainer);
},
/**
* Update the tabs positions
*
* @param tabs - All existing tabs in the kanban
* @param moveToIndex - The current Active tab in the index
* @param $tabsContainer - the jquery container of the tabs
* @private
*/
_computeTabScrollPosition(tabs, moveToIndex, $tabsContainer) {
if (tabs.length) {
const lastItemIndex = tabs.length - 1;
let scrollToLeft = 0;
for (let i = 0; i < moveToIndex; i++) {
const columnWidth = this._getTabWidth(tabs[i]);
// apply
if (moveToIndex !== lastItemIndex && i === moveToIndex - 1) {
const partialWidth = 0.75;
scrollToLeft += columnWidth * partialWidth;
} else {
scrollToLeft += columnWidth;
}
}
// Apply the scroll x on the tabs
// XXX in case of RTL, should we use scrollRight?
$tabsContainer.scrollLeft(scrollToLeft);
}
},
/**
* Compute the justify content of the kanban tab headers
* @param tabs - All existing tabs in the kanban
* @param $tabsContainer - the jquery container of the tabs
* @private
*/
_computeTabJustification(tabs, $tabsContainer) {
if (tabs.length) {
// Use to compute the sum of the width of all tab
const widthChilds = tabs.reduce((total, column) => total + this._getTabWidth(column), 0);
// Apply a space around between child if the parent length is higher then the sum of the child width
$tabsContainer.toggleClass('justify-content-between', $tabsContainer.outerWidth() >= widthChilds);
}
},
/**
* Retrieve the outerWidth of a given tab
*
* @param tab
* @returns {integer} outerWidth of the found column
* @abstract
*/
_getTabWidth(tab) {
}
};
return KanbanTabsMobileMixin;
});

View File

@@ -0,0 +1,68 @@
odoo.define('web_enterprise.ListControllerMobile', function (require) {
"use strict";
const config = require('web.config');
if (!config.device.isMobile) {
return;
}
const ListController = require('web.ListController');
ListController.include({
events: Object.assign({}, ListController.prototype.events, {
'click .o_discard_selection': '_onDiscardSelection'
}),
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* In mobile, we hide the "export" button.
*
* @override
*/
renderButtons() {
this._super(...arguments);
this.$buttons.find('.o_list_export_xlsx').hide();
},
/**
* In mobile, we hide the "export" button.
*
* @override
*/
updateButtons() {
this._super(...arguments);
this.$buttons.find('.o_list_export_xlsx').hide();
},
/**
* In mobile, we let the selection banner be added to the ControlPanel to enable the ActionMenus.
*
* @override
*/
async updateControlPanel() {
const value = await this._super(...arguments);
const displayBanner = Boolean(this.$selectionBox);
if (displayBanner) {
this._controlPanelWrapper.el.querySelector('.o_cp_bottom').prepend(this.$selectionBox[0]);
}
return value;
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Discard the current selection by unselecting any selected records.
*
* @private
*/
_onDiscardSelection() {
this.renderer.$('tbody .o_list_record_selector input:not(":disabled")').prop('checked', false);
this.renderer._updateSelection();
}
});
});

View File

@@ -0,0 +1,77 @@
odoo.define('web_enterprise.ListRenderer', function (require) {
"use strict";
const config = require('web.config');
if (config.device.isMobile) {
return;
}
const { qweb } = require('web.core');
const ListRenderer = require('web.ListRenderer');
const ListView = require('web.ListView');
const session = require('web.session');
const { patch } = require('web.utils');
const PromoteStudioDialog = require('web_enterprise.PromoteStudioDialog');
patch(ListView.prototype, 'web_enterprise.ListView', {
init: function (viewInfo, params) {
this._super(viewInfo, params);
this.rendererParams.isStudioEditable = params.action && !!params.action.xml_id;
},
});
patch(ListRenderer.prototype, 'web_enterprise.ListRenderer', {
init: function (parent, state, params) {
this._super(...arguments);
this.isStudioEditable = params.isStudioEditable;
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* This function adds a button at the bottom of the optional
* columns dropdown menu. This button opens studio if installed and
* promote studio if not installed.
*
* @override
*/
_renderOptionalColumnsDropdown: function () {
const $optionalColumnsDropdown = this._super(...arguments);
if (session.is_system && this.isStudioEditable) {
const $dropdownMenu = $optionalColumnsDropdown.find('.dropdown-menu');
if (this.optionalColumns.length) {
$dropdownMenu.append($("<hr />"));
}
const $addCustomField = $(qweb.render('web_enterprise.open_studio_button'));
$dropdownMenu.append($addCustomField);
$addCustomField.click(this._onAddCustomFieldClick.bind(this));
}
return $optionalColumnsDropdown;
},
/**
* This function returns if the optional columns dropdown menu should be rendered.
* This function returns true iff there are optional columns or the user is system
* admin.
*
* @override
*/
_shouldRenderOptionalColumnsDropdown: function () {
return this._super(...arguments) || session.is_system;
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* This function opens studio dialog
*
* @param {Event} event
* @private
*/
_onAddCustomFieldClick: function (event) {
event.stopPropagation();
new PromoteStudioDialog(this).open();
},
});
});

View File

@@ -0,0 +1,186 @@
odoo.define('web_enterprise.MobileListRenderer', function (require) {
"use strict";
const config = require('web.config');
if (!config.device.isMobile) {
return;
}
const ListRenderer = require('web.ListRenderer');
ListRenderer.include({
events: Object.assign({}, ListRenderer.prototype.events, {
'touchstart .o_data_row': '_onTouchStartSelectionMode',
'touchmove .o_data_row': '_onTouchMoveSelectionMode',
'touchend .o_data_row': '_onTouchEndSelectionMode',
}),
init() {
this._super(...arguments);
this.longTouchTimer = null;
this.LONG_TOUCH_THRESHOLD = 400;
this._onClickCapture = this._onClickCapture.bind(this);
this._ignoreEventInSelectionMode = this._ignoreEventInSelectionMode.bind(this);
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* In mobile, disable the read-only editable list.
*
* @override
*/
isEditable() {
return this.editable;
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Returns the jQuery node used to update the selection ; visiblity insensitive.
*
* @override
*/
_getSelectableRecordCheckboxes() {
return this.$('tbody .o_list_record_selector input:not(:disabled)');
},
/**
* In mobile, disable the read-only editable list.
*
* @override
*/
_isRecordEditable() {
return this.editable;
},
/**
* Reset the current long-touch timer.
*
* @private
*/
_resetLongTouchTimer() {
if (this.longTouchTimer) {
clearTimeout(this.longTouchTimer);
this.longTouchTimer = null;
}
},
/**
* @override
*/
_updateSelection() {
this._super(...arguments);
this._getSelectableRecordCheckboxes()
.each((index, input) => {
$(input).closest('.o_data_row').toggleClass('o_data_row_selected', input.checked);
});
},
_isInSelectionMode(ev) {
return !!this.selection.length;
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Prevent from opening the record on click when in selection mode.
*
* @override
*/
_onRowClicked(ev) {
if (this.selection.length) {
ev.preventDefault();
return;
}
this._super(...arguments);
},
/**
* Following @see _onTouchStartSelectionMode, we cancel the long-touch if it was shorter
* than @see LONG_TOUCH_THRESHOLD.
*
* @private
*/
_onTouchEndSelectionMode() {
const elapsedTime = Date.now() - this.touchStartMs;
if (elapsedTime < this.LONG_TOUCH_THRESHOLD) {
this._resetLongTouchTimer();
}
},
/**
* Following @see _onTouchStartSelectionMode, we cancel the long-touch.
*
* @private
*/
_onTouchMoveSelectionMode() {
this._resetLongTouchTimer();
},
/**
* We simulate a long-touch on a row by delaying (@see LONG_TOUCH_THRESHOLD)
* the actual handler and potentially cancelling it if the user moves or end
* its 'touch' before the timer's end.
*
* @private
* @param ev
*/
_onTouchStartSelectionMode(ev) {
this.touchStartMs = Date.now();
if (this.longTouchTimer === null) {
this.longTouchTimer = setTimeout(() => {
$(ev.currentTarget).find('.o_list_record_selector').click();
this._resetLongTouchTimer();
}, this.LONG_TOUCH_THRESHOLD);
}
},
_ignoreEventInSelectionMode(ev) {
if (this._isInSelectionMode()) {
ev.stopPropagation();
ev.preventDefault();
}
},
_onClickCapture(ev) {
if (!this._isInSelectionMode()) {
return;
}
const currentRow = $(ev.target).closest('.o_data_row');
if (!currentRow.length) {
return;
}
if (!ev.target.classList.contains('o_list_record_selector')) {
ev.stopPropagation();
ev.preventDefault();
$('.o_list_record_selector', currentRow).click();
}
},
_delegateEvents() {
this._super(...arguments);
this.$el[0].addEventListener('click', this._onClickCapture, { capture: true });
['mouseover', 'mouseout'].forEach(name =>
this.$el[0].addEventListener(name, this._ignoreEventInSelectionMode, { capture: true }));
},
_undelegateEvents() {
this._super(...arguments);
this.$el[0].removeEventListener('click', this._onClickCapture, { capture: true });
['mouseover', 'mouseout'].forEach(name =>
this.$el[0].removeEventListener(name, this._ignoreEventInSelectionMode, { capture: true }));
},
});
});

View File

@@ -0,0 +1,125 @@
odoo.define('web_enterprise.relational_fields', function (require) {
"use strict";
var config = require('web.config');
if (!config.device.isMobile) {
return;
}
/**
* In this file, we override some relational fields to improve the UX in mobile.
*/
var core = require('web.core');
var relational_fields = require('web.relational_fields');
var FieldStatus = relational_fields.FieldStatus;
var FieldMany2One = relational_fields.FieldMany2One;
var FieldX2Many = relational_fields.FieldX2Many;
var qweb = core.qweb;
FieldStatus.include({
/**
* Override the custom behavior of FieldStatus to hide it if it is not set,
* in mobile (which is the default behavior for fields).
*
* @returns {boolean}
*/
isEmpty: function () {
return !this.isSet();
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @override
* @private
*/
_render: function () {
this.$el.html(qweb.render("FieldStatus.content.mobile", {
selection: this.status_information,
status: _.findWhere(this.status_information, {selected: true}),
clickable: this.isClickable,
}));
},
});
/**
* Override the Many2One to prevent autocomplete and open kanban view in mobile for search.
*/
FieldMany2One.include({
start: function () {
var superRes = this._super.apply(this, arguments);
this.$input.prop('readonly', true);
return superRes;
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Don't bind autocomplete in the mobile as it uses a different mechanism
* On clicking Many2One will directly open popup with kanban view
*
* @private
* @override
*/
_bindAutoComplete: function () {},
/**
* override to add selectionMode option to search create popup option
*
* @private
* @override
*/
_getSearchCreatePopupOptions: function () {
var self = this;
var searchCreatePopupOptions = this._super.apply(this, arguments);
_.extend(searchCreatePopupOptions, {
selectionMode: true,
on_clear: function () {
self.reinitialize(false);
},
});
return searchCreatePopupOptions;
},
/**
* We always open Many2One search dialog for select/update field value
* instead of autocomplete
*
* @private
* @override
*/
_toggleAutoComplete: function () {
this._searchCreatePopup("search");
},
});
FieldX2Many.include({
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @override
* @private
*/
_renderButtons: function () {
var result = this._super.apply(this, arguments);
if (this.$buttons) {
this.$buttons
.find('.btn-secondary')
.removeClass('btn-secondary')
.addClass('btn-primary btn-add-record');
}
return result;
}
});
});

View File

@@ -0,0 +1,67 @@
odoo.define('web_enterprise.view_dialogs', function (require) {
"use strict";
var config = require('web.config');
if (!config.device.isMobile) {
return;
}
var core = require('web.core');
var view_dialogs = require('web.view_dialogs');
var _t = core._t;
view_dialogs.FormViewDialog.include({
/**
* @override
*/
init(parent, options) {
options.headerButtons = options.headerButtons || [];
this._super.apply(this, arguments);
},
/**
* Set the "Remove" button to the dialog's header buttons set
*
* @override
*/
_setRemoveButtonOption(options, btnClasses) {
options.headerButtons.push({
text: _t("Remove"),
classes: `btn-secondary ${btnClasses}`,
click: async () => {
await this._remove();
this.close();
},
});
},
});
view_dialogs.SelectCreateDialog.include({
init: function () {
this._super.apply(this,arguments);
this.on_clear = this.options.on_clear || (function () {});
this.viewType = 'kanban';
},
/**
* @override
*/
_prepareButtons: function () {
this._super.apply(this, arguments);
if (this.options.disable_multiple_selection) {
if (this.options.selectionMode) {
this.headerButtons.push({
text: _t("Clear"),
classes: 'btn-secondary o_clear_button',
close: true,
click: function () {
this.on_clear();
},
});
}
} else {
this.__buttons = this.__buttons.filter(button => !button.classes.split(' ').includes('o_select_button'));
}
},
});
});

View File

@@ -0,0 +1,96 @@
odoo.define('web_enterprise.PromoteStudioDialog', function (require) {
"use strict";
const core = require('web.core');
const Dialog = require('web.Dialog');
const framework = require('web.framework');
const localStorage = require('web.local_storage');
const qweb = core.qweb;
const PromoteStudioDialog = Dialog.extend({
events: _.extend({}, Dialog.prototype.events, {
'click button.o_install_studio': '_onInstallStudio',
}),
/**
* This init function adds a click listener on window to handle
* modal closing.
* @override
*/
init: function (parent, options) {
options = _.defaults(options || {}, {
$content: $(qweb.render('web_enterprise.install_web_studio')),
renderHeader: false,
renderFooter: false,
size: 'large',
});
this._super(parent, options);
},
/**
* This function adds an handler for window clicks at the end of start.
*
* @override
*/
start: async function () {
await this._super.apply(this, arguments);
core.bus.on('click', this, this._onWindowClick);
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* This function both installs studio and reload the current view in studio mode.
*
* @param {Event} ev
*/
_onInstallStudio: async function (event) {
event.stopPropagation();
this.disableClick = true;
framework.blockUI();
const modules = await this._rpc({
model: 'ir.module.module',
method: 'search_read',
fields: ['id'],
domain: [['name', '=', 'web_studio']],
});
await this._rpc({
model: 'ir.module.module',
method: 'button_immediate_install',
args: [[modules[0].id]],
});
// on rpc call return, the framework unblocks the page
// make sure to keep the page blocked until the reload ends.
framework.blockUI();
localStorage.setItem('openStudioOnReload', 'main');
this._reloadPage();
},
/**
* Close modal when the user clicks outside the modal WITHOUT propagating
* the event.
* We must use this function to keep dropdown menu open when the user clicks in the modal, too.
* This behaviour cannot be handled by using the modal backdrop.
*
* @param {Event} event
*/
_onWindowClick: function (event) {
const $modal = $(event.target).closest('.modal-studio');
if (!$modal.length && !this.disableClick) {
this._onCloseDialog(event);
}
event.stopPropagation();
},
//--------------------------------------------------------------------------
// Utils
//--------------------------------------------------------------------------
/**
* This function isolate location reload in order to easily mock it (in tests for example)
*
* @private
*/
_reloadPage: function () {
window.location.reload();
},
});
return PromoteStudioDialog;
});