[FW][MERGE] web,*: introduce Layout component
This PR introduces a new component: the layout component. Its purpose is to make abstraction of most generic components and classes when writing a view template or an action. To make use of this component, we also needed a new system to pass slot from the parent of the layout to the control panel. This was done in a hacky way and is meant to be replaced as soon as Owl provides a built-in way to pass slots. The control panel has also been slightly tweaked to default its props from an environment's newly introduced key: the 'config'. This has been done to reduce the amount of props passed to the layout and search subcomponents. closes odoo/odoo#77463 Forward-port-of: odoo/odoo#76140 Related: odoo/enterprise#21295 Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
This commit is contained in:
@@ -36,10 +36,10 @@ ImportRecords.template = "base_import.ImportRecords";
|
||||
const importRecordsItem = {
|
||||
Component: ImportRecords,
|
||||
groupNumber: 4,
|
||||
isDisplayed: ({ isSmall, searchModel }) =>
|
||||
isDisplayed: ({ config, isSmall }) =>
|
||||
!isSmall &&
|
||||
searchModel.action.type === "ir.actions.act_window" &&
|
||||
["kanban", "list"].includes(searchModel.view.type)
|
||||
config.actionType === "ir.actions.act_window" &&
|
||||
["kanban", "list"].includes(config.viewType)
|
||||
// TODO: add arch info to searchModel?
|
||||
// !!JSON.parse(env.view.arch.attrs.import || "1") &&
|
||||
// !!JSON.parse(env.view.arch.attrs.create || "1"),
|
||||
|
||||
@@ -24,7 +24,7 @@ export class AddToBoard extends Component {
|
||||
setup() {
|
||||
this.notification = useService("notification");
|
||||
this.rpc = useService("rpc");
|
||||
this.state = useState({ name: this.env.searchModel.displayName });
|
||||
this.state = useState({ name: this.env.config.displayName });
|
||||
|
||||
useAutofocus();
|
||||
}
|
||||
@@ -34,39 +34,19 @@ export class AddToBoard extends Component {
|
||||
//---------------------------------------------------------------------
|
||||
|
||||
async addToBoard() {
|
||||
const {
|
||||
action,
|
||||
displayName,
|
||||
domain,
|
||||
context,
|
||||
groupBy,
|
||||
orderedBy,
|
||||
view,
|
||||
} = this.env.searchModel;
|
||||
|
||||
// Retrieves view context
|
||||
const fns = this.env.__getContext__.callbacks;
|
||||
const viewContext = Object.assign({}, ...fns.map((fn) => fn()));
|
||||
|
||||
const { context, domain } = this.env.searchModel.getIrFilterValues();
|
||||
const contextToSave = {
|
||||
...context,
|
||||
group_by: groupBy,
|
||||
orderedBy,
|
||||
orderedBy: this.env.searchModel.orderBy,
|
||||
dashboard_merge_domains_contexts: false,
|
||||
...viewContext,
|
||||
};
|
||||
|
||||
const detailedComparison = this.env.searchModel.getFullComparison();
|
||||
if (detailedComparison) {
|
||||
contextToSave.comparison = detailedComparison;
|
||||
}
|
||||
|
||||
const result = await this.rpc("/board/add_to_dashboard", {
|
||||
action_id: action.id,
|
||||
action_id: this.env.config.actionId,
|
||||
context_to_save: contextToSave,
|
||||
domain,
|
||||
name: this.state.name,
|
||||
view_mode: view.type,
|
||||
view_mode: this.env.config.viewType,
|
||||
});
|
||||
|
||||
if (result) {
|
||||
@@ -77,12 +57,11 @@ export class AddToBoard extends Component {
|
||||
type: "warning",
|
||||
}
|
||||
);
|
||||
this.state.name = displayName;
|
||||
this.state.name = this.env.config.displayName;
|
||||
} else {
|
||||
this.notification.add(
|
||||
this.env._t("Could not add filter to dashboard"),
|
||||
{ type: "danger" }
|
||||
);
|
||||
this.notification.add(this.env._t("Could not add filter to dashboard"), {
|
||||
type: "danger",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +85,7 @@ AddToBoard.template = "board.AddToBoard";
|
||||
const addToBoardItem = {
|
||||
Component: AddToBoard,
|
||||
groupNumber: 4,
|
||||
isDisplayed: ({ searchModel }) => searchModel.action.type === "ir.actions.act_window",
|
||||
isDisplayed: ({ config }) => config.actionType === "ir.actions.act_window",
|
||||
};
|
||||
|
||||
favoriteMenuRegistry.add("add-to-board", addToBoardItem, { sequence: 10 });
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,24 +1,23 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { _lt } from "@web/core/l10n/translation";
|
||||
import AbstractView from "web.AbstractView";
|
||||
import AbstractModel from "web.AbstractModel";
|
||||
import { controlPanel as cpHelpers } from "web.test_utils";
|
||||
import { createWebClient, doAction } from "@web/../tests/webclient/helpers";
|
||||
import { dialogService } from "@web/core/dialog/dialog_service";
|
||||
import { legacyExtraNextTick, patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
import legacyViewRegistry from "web.view_registry";
|
||||
import { makeView } from "@web/../tests/views/helpers";
|
||||
import { mock } from "web.test_utils";
|
||||
import { registry } from "@web/core/registry";
|
||||
import {
|
||||
setupControlPanelServiceRegistry,
|
||||
switchView,
|
||||
toggleFilterMenu,
|
||||
toggleGroupByMenu,
|
||||
toggleMenuItem,
|
||||
toggleMenuItemOption,
|
||||
toggleMenuItemOption
|
||||
} from "@web/../tests/search/helpers";
|
||||
import { makeView } from "@web/../tests/views/helpers";
|
||||
import { createWebClient, doAction } from "@web/../tests/webclient/helpers";
|
||||
import { dialogService } from "@web/core/dialog/dialog_service";
|
||||
import { _lt } from "@web/core/l10n/translation";
|
||||
import { registry } from "@web/core/registry";
|
||||
import AbstractModel from "web.AbstractModel";
|
||||
import AbstractView from "web.AbstractView";
|
||||
import { controlPanel as cpHelpers, mock } from "web.test_utils";
|
||||
import legacyViewRegistry from "web.view_registry";
|
||||
|
||||
const patchDate = mock.patchDate;
|
||||
|
||||
@@ -88,7 +87,7 @@ QUnit.module("Views", (hooks) => {
|
||||
|
||||
const forecastGraph = await makeView({
|
||||
resModel: "foo",
|
||||
type: "forecast_graph",
|
||||
type: "graph",
|
||||
serverData,
|
||||
searchViewId: false,
|
||||
context: {
|
||||
@@ -133,7 +132,7 @@ QUnit.module("Views", (hooks) => {
|
||||
|
||||
await makeView({
|
||||
resModel: "foo",
|
||||
type: "forecast_graph",
|
||||
type: "graph",
|
||||
serverData,
|
||||
searchViewId: false,
|
||||
context: {
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ AddToGoogleSpreadsheet.template = "google_spreadsheet.AddToGoogleSpreadsheet";
|
||||
const addToGoogleSpreadsheetItem = {
|
||||
Component: AddToGoogleSpreadsheet,
|
||||
groupNumber: 4,
|
||||
isDisplayed: ({ searchModel }) => searchModel.action.type === "ir.actions.act_window",
|
||||
isDisplayed: ({ config }) => config.actionType === "ir.actions.act_window",
|
||||
};
|
||||
|
||||
favoriteMenuRegistry.add("add-to-google-spreadsheet", addToGoogleSpreadsheetItem, { sequence: 20 });
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { GraphView } from "@web/views/graph/graph_view";
|
||||
import { ProjectControlPanel } from "@project/project_control_panel/project_control_panel";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { GraphView } from "@web/views/graph/graph_view";
|
||||
|
||||
const viewRegistry = registry.category("views");
|
||||
|
||||
class ProjectGraphView extends GraphView {}
|
||||
ProjectGraphView.components = { ...GraphView.components, ControlPanel: ProjectControlPanel };
|
||||
ProjectGraphView.ControlPanel = ProjectControlPanel;
|
||||
|
||||
viewRegistry.add("project_graph", ProjectGraphView);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { PivotView } from "@web/views/pivot/pivot_view";
|
||||
import { ProjectControlPanel } from "@project/project_control_panel/project_control_panel";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { PivotView } from "@web/views/pivot/pivot_view";
|
||||
|
||||
const viewRegistry = registry.category("views");
|
||||
|
||||
class ProjectPivotView extends PivotView {}
|
||||
ProjectPivotView.components = { ...PivotView.components, ControlPanel: ProjectControlPanel };
|
||||
ProjectPivotView.ControlPanel = ProjectControlPanel;
|
||||
|
||||
viewRegistry.add("project_pivot", ProjectPivotView);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { _lt } from "@web/core/l10n/translation";
|
||||
import { GraphView } from "@web/views/graph/graph_view";
|
||||
import { GraphArchParser } from "@web/views/graph/graph_arch_parser";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { GraphArchParser } from "@web/views/graph/graph_arch_parser";
|
||||
import { GraphView } from "@web/views/graph/graph_view";
|
||||
|
||||
const viewRegistry = registry.category("views");
|
||||
|
||||
@@ -29,6 +29,6 @@ class ProjectRatingArchParser extends GraphArchParser {
|
||||
// Would it be not better achiedved by using a proper arch directly?
|
||||
|
||||
class ProjectRatingGraphView extends GraphView {}
|
||||
ProjectRatingGraphView.archParser = ProjectRatingArchParser;
|
||||
ProjectRatingGraphView.ArchParser = ProjectRatingArchParser;
|
||||
|
||||
viewRegistry.add("project_rating_graph", ProjectRatingGraphView);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { _lt } from "@web/core/l10n/translation";
|
||||
import { PivotView } from "@web/views/pivot/pivot_view";
|
||||
import { PivotArchParser } from "@web/views/pivot/pivot_arch_parser";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { PivotArchParser } from "@web/views/pivot/pivot_arch_parser";
|
||||
import { PivotView } from "@web/views/pivot/pivot_view";
|
||||
|
||||
const viewRegistry = registry.category("views");
|
||||
|
||||
@@ -29,6 +29,6 @@ class ProjectRatingArchParser extends PivotArchParser {
|
||||
// Would it be not better achiedved by using a proper arch directly?
|
||||
|
||||
class ProjectRatingPivotView extends PivotView {}
|
||||
ProjectRatingPivotView.archParser = ProjectRatingArchParser;
|
||||
ProjectRatingPivotView.ArchParser = ProjectRatingArchParser;
|
||||
|
||||
viewRegistry.add("project_rating_pivot", ProjectRatingPivotView);
|
||||
|
||||
@@ -9,7 +9,7 @@ export class ProjectControlPanel extends ControlPanel {
|
||||
this.orm = useService("orm");
|
||||
this.user = useService("user");
|
||||
const { active_id, show_project_update } = this.env.searchModel.globalContext;
|
||||
this.showProjectUpdate = this.env.searchModel.view.type === "form" || show_project_update;
|
||||
this.showProjectUpdate = this.env.config.viewType === "form" || show_project_update;
|
||||
this.projectId = this.showProjectUpdate ? active_id : false;
|
||||
}
|
||||
|
||||
@@ -48,4 +48,5 @@ export class ProjectControlPanel extends ControlPanel {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ProjectControlPanel.template = "project.ProjectControlPanel";
|
||||
|
||||
@@ -51,10 +51,11 @@ function registerView(name, LegacyView) {
|
||||
searchPanel = globalState.searchPanel;
|
||||
}
|
||||
|
||||
this.viewParams = Object.assign({}, this.props.actionFlags, {
|
||||
const { actionFlags, breadcrumbs = [] } = this.env.config;
|
||||
this.viewParams = Object.assign({}, actionFlags, {
|
||||
action: this.props.action,
|
||||
// legacy views automatically add the last part of the breadcrumbs
|
||||
breadcrumbs: breadcrumbsToLegacy(this.props.breadcrumbs),
|
||||
breadcrumbs: breadcrumbsToLegacy(breadcrumbs),
|
||||
modelName: this.props.resModel,
|
||||
currentId: this.props.resId,
|
||||
controllerState: {
|
||||
@@ -99,7 +100,7 @@ function registerView(name, LegacyView) {
|
||||
context: this.props.context,
|
||||
};
|
||||
const options = {
|
||||
actionId: this.props.action.id,
|
||||
actionId: this.env.config.actionId,
|
||||
loadActionMenus: this.props.loadActionMenus,
|
||||
loadIrFilters: this.props.loadIrFilters,
|
||||
};
|
||||
@@ -120,10 +121,11 @@ function registerView(name, LegacyView) {
|
||||
viewFields: result.fields_views.search.fields,
|
||||
});
|
||||
}
|
||||
const { viewSwitcherEntries = [] } = this.env.config;
|
||||
const views = this.viewParams.action.views
|
||||
.filter(([vid, vtype]) => vtype !== "search")
|
||||
.filter(([, vtype]) => vtype !== "search")
|
||||
.map(([vid, vtype]) => {
|
||||
const view = this.props.viewSwitcherEntries.find((v) => v.type === vtype);
|
||||
const view = viewSwitcherEntries.find((v) => v.type === vtype);
|
||||
if (view) {
|
||||
return Object.assign({}, view, { viewID: vid });
|
||||
} else {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { ComparisonMenu } from "../comparison_menu/comparison_menu";
|
||||
import { FavoriteMenu } from "../favorite_menu/favorite_menu";
|
||||
import { FilterMenu } from "../filter_menu/filter_menu";
|
||||
import { GroupByMenu } from "../group_by_menu/group_by_menu";
|
||||
import { SearchBar } from "../search_bar/search_bar";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
@@ -21,6 +21,44 @@ export class ControlPanel extends Component {
|
||||
this.actionService = useService("action");
|
||||
}
|
||||
|
||||
/**
|
||||
* !!! What follows is a hack, do not copy it !!!
|
||||
*
|
||||
* Duplicates the slots defined for the parent s.t. they are also available
|
||||
* for the current control panel.
|
||||
*
|
||||
* This hack is necessary since Owl does not support manual slots
|
||||
* assignment/transfer yet. This must be removed as soon as Owl implements
|
||||
* such a system.
|
||||
*
|
||||
* @strongly_discouraged_override
|
||||
*/
|
||||
__render() {
|
||||
const { slots } = this.env.qweb.constructor;
|
||||
const { __owl__ } = this;
|
||||
const originalSlots = {};
|
||||
const transferredSlotNames = [
|
||||
"control-panel-top-left",
|
||||
"control-panel-top-right",
|
||||
"control-panel-bottom-left",
|
||||
"control-panel-bottom-right",
|
||||
];
|
||||
for (const slotName of transferredSlotNames) {
|
||||
const parentSlotkey = `${__owl__.parent.__owl__.slotId}_${slotName}`;
|
||||
if (parentSlotkey in slots) {
|
||||
const cpSlotKey = `${__owl__.slotId}_${slotName}`;
|
||||
originalSlots[cpSlotKey] = slots[cpSlotKey];
|
||||
slots[cpSlotKey] = function (scope, extra) {
|
||||
slots[parentSlotkey].call(this, __owl__.parent.__owl__.scope, extra);
|
||||
};
|
||||
}
|
||||
}
|
||||
const res = super.__render(...arguments);
|
||||
// Clean up
|
||||
Object.assign(slots, originalSlots);
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Object}
|
||||
*/
|
||||
@@ -78,25 +116,3 @@ export class ControlPanel extends Component {
|
||||
|
||||
ControlPanel.components = { ComparisonMenu, FavoriteMenu, FilterMenu, GroupByMenu, SearchBar };
|
||||
ControlPanel.template = "web.ControlPanel";
|
||||
// ControlPanel.props = {
|
||||
// breadcrumbs: { type: Array, element: { jsId: String, name: String }, optional: true },
|
||||
// display: { type: Object, optional: true },
|
||||
// displayName: { type: String, optional: true },
|
||||
// viewSwitcherEntries: {
|
||||
// type: Array,
|
||||
// element: {
|
||||
// type: Object,
|
||||
// shape: {
|
||||
// active: { type: Boolean, optional: true },
|
||||
// icon: String,
|
||||
// multiRecord: { type: Boolean, optional: true },
|
||||
// name: [Object, String],
|
||||
// type: String,
|
||||
// },
|
||||
// },
|
||||
// optional: true,
|
||||
// },
|
||||
// };
|
||||
ControlPanel.defaultProps = {
|
||||
breadcrumbs: [],
|
||||
};
|
||||
|
||||
@@ -30,9 +30,9 @@
|
||||
|
||||
<div class="o_cp_pager"/>
|
||||
|
||||
<t t-if="props.viewSwitcherEntries and props.viewSwitcherEntries.length > 1">
|
||||
<t t-if="(env.config.viewSwitcherEntries or []).length">
|
||||
<nav class="btn-group o_cp_switch_buttons">
|
||||
<t t-foreach="props.viewSwitcherEntries" t-as="view" t-key="view.type">
|
||||
<t t-foreach="env.config.viewSwitcherEntries" t-as="view" t-key="view.type">
|
||||
<button class="btn btn-secondary fa fa-lg o_switch_view "
|
||||
t-attf-class="o_{{view.type}} {{view.icon}} {{view.active ? 'active' : ''}}"
|
||||
t-att-data-tooltip="view.name"
|
||||
@@ -48,7 +48,7 @@
|
||||
|
||||
<t t-name="web.Breadcrumbs" owl="1">
|
||||
<ol class="breadcrumb">
|
||||
<t t-foreach="props.breadcrumbs or []" t-as="breadcrumb" t-key="breadcrumb.jsId">
|
||||
<t t-foreach="env.config.breadcrumbs or []" t-as="breadcrumb" t-key="breadcrumb.jsId">
|
||||
<li class="breadcrumb-item"
|
||||
t-att-class="{ o_back_button: breadcrumb_last}"
|
||||
t-on-click.prevent="onBreadcrumbClicked(breadcrumb.jsId)"
|
||||
@@ -59,7 +59,7 @@
|
||||
</li>
|
||||
</t>
|
||||
<li class="breadcrumb-item active">
|
||||
<t t-if="props.displayName" t-esc="props.displayName"/>
|
||||
<t t-if="env.config.displayName" t-esc="env.config.displayName" />
|
||||
<em t-else="" class="text-warning">Unnamed</em>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
@@ -14,7 +14,7 @@ export class CustomFavoriteItem extends Component {
|
||||
this.descriptionRef = useRef("description");
|
||||
useAutofocus();
|
||||
this.state = useState({
|
||||
description: this.env.searchModel.displayName,
|
||||
description: this.env.config.displayName,
|
||||
isDefault: false,
|
||||
isShared: false,
|
||||
});
|
||||
@@ -46,7 +46,7 @@ export class CustomFavoriteItem extends Component {
|
||||
this.env.searchModel.createNewFavorite({ description, isDefault, isShared });
|
||||
|
||||
Object.assign(this.state, {
|
||||
description: this.env.searchModel.displayName,
|
||||
description: this.env.config.displayName,
|
||||
isDefault: false,
|
||||
isShared: false,
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import { makeContext } from "@web/core/context";
|
||||
import { Domain } from "@web/core/domain";
|
||||
import { evaluateExpr } from "@web/core/py_js/py";
|
||||
import { sortBy } from "@web/core/utils/arrays";
|
||||
import { deepCopy } from "@web/core/utils/objects";
|
||||
import { SearchArchParser } from "./search_arch_parser";
|
||||
import {
|
||||
constructDateDomain,
|
||||
@@ -15,7 +16,6 @@ import {
|
||||
yearSelected,
|
||||
} from "./utils/dates";
|
||||
import { FACET_ICONS } from "./utils/misc";
|
||||
import { deepCopy } from "@web/core/utils/objects";
|
||||
|
||||
const { DateTime } = luxon;
|
||||
const EventBus = owl.core.EventBus;
|
||||
@@ -190,11 +190,9 @@ export class SearchModel extends EventBus {
|
||||
* @param {number|false} [config.searchViewId=false]
|
||||
* @param {Object[]} [config.irFilters=[]]
|
||||
*
|
||||
* @param {Object} [config.action={id:false,views:[]}]
|
||||
* @param {boolean} [config.activateFavorite=true]
|
||||
* @param {Object | null} [config.comparison]
|
||||
* @param {Object} [config.context={}]
|
||||
* @param {String} [config.displayName=""]
|
||||
* @param {Array} [config.domain=[]]
|
||||
* @param {Array} [config.dynamicFilters=[]]
|
||||
* @param {string[]} [config.groupBy=[]]
|
||||
@@ -203,7 +201,6 @@ export class SearchModel extends EventBus {
|
||||
* @param {string[]} [config.orderBy=[]]
|
||||
* @param {string[]} [config.searchMenuTypes=["filter", "groupBy", "favorite"]]
|
||||
* @param {Object} [config.state]
|
||||
* @param {Object} [config.view={id:false}]
|
||||
*/
|
||||
async load(config) {
|
||||
const { resModel } = config;
|
||||
@@ -212,12 +209,6 @@ export class SearchModel extends EventBus {
|
||||
}
|
||||
this.resModel = resModel;
|
||||
|
||||
const { action, displayName, view } = config;
|
||||
|
||||
this.action = action || { id: false, views: [] };
|
||||
this.displayName = displayName || "";
|
||||
this.view = view || { id: false };
|
||||
|
||||
// used to avoid useless recomputations
|
||||
this._reset();
|
||||
|
||||
@@ -245,7 +236,7 @@ export class SearchModel extends EventBus {
|
||||
views: [[searchViewId, "search"]],
|
||||
},
|
||||
{
|
||||
actionId: this.action.id,
|
||||
actionId: this.env.config.actionId,
|
||||
loadIrFilters: loadIrFilters || false,
|
||||
}
|
||||
);
|
||||
@@ -322,7 +313,6 @@ export class SearchModel extends EventBus {
|
||||
const { labels, preSearchItems, searchPanelInfo, sections } = parser.parse();
|
||||
|
||||
this.searchPanelInfo = { ...searchPanelInfo, shouldReload: false };
|
||||
this.display = this._getDisplay(config.display);
|
||||
|
||||
await Promise.all(labels.map((cb) => cb(this.orm)));
|
||||
|
||||
@@ -355,6 +345,7 @@ export class SearchModel extends EventBus {
|
||||
|
||||
/** @type Map<number,Section> */
|
||||
this.sections = new Map(sections || []);
|
||||
this.display = this._getDisplay(config.display);
|
||||
|
||||
if (this.display.searchPanel) {
|
||||
/** @type DomainListRepr */
|
||||
@@ -1381,11 +1372,14 @@ export class SearchModel extends EventBus {
|
||||
*/
|
||||
_getDisplay(display = {}) {
|
||||
const { viewTypes } = this.searchPanelInfo;
|
||||
const { bannerRoute, viewType } = this.env.config;
|
||||
return {
|
||||
controlPanel: "controlPanel" in display ? display.controlPanel : {},
|
||||
searchPanel:
|
||||
(!this.view.type || viewTypes.includes(this.view.type)) &&
|
||||
this.sections.size &&
|
||||
(!viewType || viewTypes.includes(viewType)) &&
|
||||
("searchPanel" in display ? display.searchPanel : true),
|
||||
banner: Boolean(bannerRoute),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1763,7 +1757,7 @@ export class SearchModel extends EventBus {
|
||||
};
|
||||
const irFilter = {
|
||||
name: description,
|
||||
action_id: this.action.id,
|
||||
action_id: this.env.config.actionId,
|
||||
model_id: this.resModel,
|
||||
domain,
|
||||
is_default: isDefault,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { useBus, useEffect, useService } from "@web/core/utils/hooks";
|
||||
import { useBus, useService } from "@web/core/utils/hooks";
|
||||
import { SearchModel } from "@web/search/search_model";
|
||||
import { CallbackRecorder, useSetupAction } from "@web/webclient/actions/action_hook";
|
||||
|
||||
@@ -15,40 +15,24 @@ export class WithSearch extends Component {
|
||||
this.Component = this.props.Component;
|
||||
|
||||
if (!this.env.__getContext__) {
|
||||
useSubEnv({
|
||||
__getContext__: new CallbackRecorder(),
|
||||
});
|
||||
useSubEnv({ __getContext__: new CallbackRecorder() });
|
||||
}
|
||||
|
||||
const SearchModelClass = this.Component.SearchModel || SearchModel;
|
||||
this.searchModel = new SearchModelClass(this.env, {
|
||||
this.env.searchModel = new SearchModelClass(this.env, {
|
||||
user: useService("user"),
|
||||
orm: useService("orm"),
|
||||
view: useService("view"),
|
||||
});
|
||||
useBus(this.searchModel, "update", () => this.render());
|
||||
useSubEnv({
|
||||
searchModel: this.searchModel,
|
||||
});
|
||||
|
||||
useBus(this.env.searchModel, "update", this.render);
|
||||
useSetupAction({
|
||||
getGlobalState: () => {
|
||||
return {
|
||||
searchModel: JSON.stringify(this.searchModel.exportState()),
|
||||
searchModel: JSON.stringify(this.env.searchModel.exportState()),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!this.searchModel.display.searchPanel) {
|
||||
return;
|
||||
}
|
||||
// TODO: add better way to retrieve o_content
|
||||
const [content] = this.el.getElementsByClassName("o_content");
|
||||
if (content) {
|
||||
content.classList.add("o_component_with_search_panel");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async willStart() {
|
||||
@@ -57,7 +41,7 @@ export class WithSearch extends Component {
|
||||
config.state = JSON.parse(config.globalState.searchModel);
|
||||
delete config.globalState;
|
||||
}
|
||||
await this.searchModel.load(config);
|
||||
await this.env.searchModel.load(config);
|
||||
}
|
||||
|
||||
async willUpdateProps(nextProps) {
|
||||
@@ -67,7 +51,7 @@ export class WithSearch extends Component {
|
||||
config[key] = nextProps[key];
|
||||
}
|
||||
}
|
||||
await this.searchModel.reload(config);
|
||||
await this.env.searchModel.reload(config);
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------------
|
||||
@@ -77,20 +61,18 @@ export class WithSearch extends Component {
|
||||
get componentProps() {
|
||||
const componentProps = { ...this.props.componentProps };
|
||||
for (const key of SEARCH_KEYS) {
|
||||
componentProps[key] = this.searchModel[key];
|
||||
componentProps[key] = this.env.searchModel[key];
|
||||
}
|
||||
componentProps.info = componentProps.info || {};
|
||||
for (const key of OTHER_SEARCH_KEYS) {
|
||||
componentProps.info[key] = this.searchModel[key];
|
||||
componentProps.info[key] = this.env.searchModel[key];
|
||||
}
|
||||
return componentProps;
|
||||
}
|
||||
}
|
||||
|
||||
WithSearch.defaultProps = {
|
||||
action: { id: false, views: [] },
|
||||
componentProps: {},
|
||||
view: { id: false },
|
||||
};
|
||||
WithSearch.props = {
|
||||
Component: Function,
|
||||
@@ -98,25 +80,6 @@ WithSearch.props = {
|
||||
|
||||
resModel: String,
|
||||
|
||||
action: {
|
||||
type: Object,
|
||||
shape: {
|
||||
id: [Number, false],
|
||||
type: { type: [String, false], optional: true },
|
||||
views: { type: Array, element: [Number, String, false], optional: true },
|
||||
},
|
||||
optional: true,
|
||||
},
|
||||
displayName: { type: String, optional: true },
|
||||
view: {
|
||||
type: Object,
|
||||
shape: {
|
||||
id: [Number, false],
|
||||
type: { type: [String, false], optional: true },
|
||||
},
|
||||
optional: true,
|
||||
},
|
||||
|
||||
globalState: { type: Object, optional: true },
|
||||
|
||||
display: { type: Object, optional: true },
|
||||
|
||||
@@ -44,15 +44,12 @@ export function editView({ accessRights, component, env }) {
|
||||
if (!accessRights.canEditView) {
|
||||
return null;
|
||||
}
|
||||
let type;
|
||||
let { viewId } = component.props.info || {}; // fallback is there for legacy
|
||||
let { viewId, viewType: type } = component.env.config || {}; // fallback is there for legacy
|
||||
if ("viewInfo" in component.props) {
|
||||
// legacy
|
||||
viewId = component.props.viewInfo.view_id;
|
||||
type = component.props.viewInfo.type;
|
||||
type = type === "tree" ? "list" : type;
|
||||
} else {
|
||||
type = component.constructor.type;
|
||||
}
|
||||
const displayName = type[0].toUpperCase() + type.slice(1);
|
||||
const description = env._t("Edit View: ") + displayName;
|
||||
|
||||
@@ -6,16 +6,16 @@
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="o_measure" t-esc="measure"/>
|
||||
<th class="o_measure" t-esc="measure" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="tooltipItems" t-as="tooltipItem" t-key="tooltipItem.id">
|
||||
<td>
|
||||
<span class="o_square" t-attf-style="background-color: {{ tooltipItem.boxColor }}"/>
|
||||
<span class="o_label" t-attf-style="max-width: {{ maxWidth }}" t-esc="tooltipItem.label"/>
|
||||
<span class="o_square" t-attf-style="background-color: {{ tooltipItem.boxColor }}" />
|
||||
<span class="o_label" t-attf-style="max-width: {{ maxWidth }}" t-esc="tooltipItem.label" />
|
||||
</td>
|
||||
<td class="o_value" t-esc="tooltipItem.value"/>
|
||||
<td class="o_value" t-esc="tooltipItem.value" />
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -25,9 +25,9 @@
|
||||
<t t-name="web.GraphRenderer" owl="1">
|
||||
<div class="o_graph_renderer">
|
||||
<div class="o_graph_canvas_container" t-ref="container">
|
||||
<canvas t-ref="canvas"/>
|
||||
<canvas t-ref="canvas" />
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
</templates>
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { _lt } from "@web/core/l10n/translation";
|
||||
import { ControlPanel } from "@web/search/control_panel/control_panel";
|
||||
import { SearchPanel } from "@web/search/search_panel/search_panel";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { GroupByMenu } from "@web/search/group_by_menu/group_by_menu";
|
||||
import { standardViewProps } from "@web/views/helpers/standard_view_props";
|
||||
import { useSetupView } from "@web/views/helpers/view_hook";
|
||||
import { Layout } from "@web/views/layout";
|
||||
import { useModel } from "../helpers/model";
|
||||
import { GraphArchParser } from "./graph_arch_parser";
|
||||
import { GraphModel } from "./graph_model";
|
||||
import { GraphRenderer } from "./graph_renderer";
|
||||
import { GroupByMenu } from "@web/search/group_by_menu/group_by_menu";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { standardViewProps } from "@web/views/helpers/standard_view_props";
|
||||
import { useModel } from "../helpers/model";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { useSetupView } from "@web/views/helpers/view_hook";
|
||||
|
||||
const viewRegistry = registry.category("views");
|
||||
|
||||
@@ -74,7 +73,7 @@ export class GraphView extends Component {
|
||||
const { context, resModel, title } = this.model.metaData;
|
||||
|
||||
const views = {};
|
||||
for (const [viewId, viewType] of this.props.info.views || []) {
|
||||
for (const [viewId, viewType] of this.env.config.views || []) {
|
||||
views[viewType] = viewId;
|
||||
}
|
||||
function getView(viewType) {
|
||||
@@ -131,7 +130,7 @@ export class GraphView extends Component {
|
||||
GraphView.template = "web.GraphView";
|
||||
GraphView.buttonTemplate = "web.GraphView.Buttons";
|
||||
|
||||
GraphView.components = { ControlPanel, GroupByMenu, Renderer: GraphRenderer, SearchPanel };
|
||||
GraphView.components = { GroupByMenu, Renderer: GraphRenderer, Layout };
|
||||
|
||||
GraphView.defaultProps = {
|
||||
additionalMeasures: [],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.o_graph_view {
|
||||
.o_graph_view > .o_content > .o_renderer {
|
||||
height: 100%;
|
||||
|
||||
.o_graph_canvas_container {
|
||||
|
||||
@@ -46,31 +46,26 @@
|
||||
</t>
|
||||
|
||||
<t t-name="web.GraphView" owl="1">
|
||||
<div class="o_graph_view" t-att-class="{ o_view_sample_data: model.useSampleModel }">
|
||||
<ControlPanel t-if="env.searchModel.display.controlPanel" t-props="props.info">
|
||||
<t t-set-slot="control-panel-bottom-left">
|
||||
<t t-call="{{ constructor.buttonTemplate }}"/>
|
||||
<Layout viewType="'graph'" useSampleModel="model.useSampleModel">
|
||||
<t t-set-slot="control-panel-bottom-left">
|
||||
<t t-call="{{ constructor.buttonTemplate }}"/>
|
||||
</t>
|
||||
<t t-if="model.data">
|
||||
<t t-if="model.useSampleModel and props.info.noContentHelp" t-call="web.ActionHelper">
|
||||
<t t-set="noContentHelp" t-value="props.info.noContentHelp"/>
|
||||
</t>
|
||||
</ControlPanel>
|
||||
<div class="o_content">
|
||||
<SearchPanel t-if="env.searchModel.display.searchPanel" />
|
||||
<t t-if="model.data">
|
||||
<t t-if="model.useSampleModel and props.info.noContentHelp" t-call="web.ActionHelper">
|
||||
<t t-set="noContentHelp" t-value="props.info.noContentHelp"/>
|
||||
</t>
|
||||
<Renderer
|
||||
class="o_renderer"
|
||||
model="model"
|
||||
t-att-class="{ o_sample_data_disabled: model.useSampleModel }"
|
||||
onGraphClicked="(domain) => onGraphClicked(domain)"
|
||||
/>
|
||||
</t>
|
||||
<t t-else="" t-call="web.NoContentHelper">
|
||||
<t t-set="title">Invalid data</t>
|
||||
<t t-set="description">Pie chart cannot mix positive and negative numbers. Try to change your domain to only display positive results</t>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<Renderer
|
||||
class="o_renderer"
|
||||
model="model"
|
||||
t-att-class="{ o_sample_data_disabled: model.useSampleModel }"
|
||||
onGraphClicked="(domain) => onGraphClicked(domain)"
|
||||
/>
|
||||
</t>
|
||||
<t t-else="" t-call="web.NoContentHelper">
|
||||
<t t-set="title">Invalid data</t>
|
||||
<t t-set="description">Pie chart cannot mix positive and negative numbers. Try to change your domain to only display positive results</t>
|
||||
</t>
|
||||
</Layout>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { ControlPanel } from "@web/search/control_panel/control_panel";
|
||||
import { SearchPanel } from "@web/search/search_panel/search_panel";
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
/**
|
||||
* @param {Object} params
|
||||
* @returns {Object}
|
||||
*/
|
||||
export const extractLayoutComponents = (params) => {
|
||||
return {
|
||||
ControlPanel: params.ControlPanel || ControlPanel,
|
||||
SearchPanel: params.SearchPanel || SearchPanel,
|
||||
Banner: params.Banner || false,
|
||||
};
|
||||
};
|
||||
|
||||
export class Layout extends Component {
|
||||
setup() {
|
||||
const { display = {} } = this.env.searchModel || {};
|
||||
this.components = extractLayoutComponents(this.env.config);
|
||||
this.display = display;
|
||||
}
|
||||
}
|
||||
|
||||
Layout.template = "web.Layout";
|
||||
Layout.props = {
|
||||
viewType: { type: String, optional: true },
|
||||
useSampleModel: { type: Boolean, optional: true },
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="web.Layout" owl="1">
|
||||
<div t-att-class="{ o_view_sample_data: props.useSampleModel }" t-attf-class="{{ props.viewType ? `o_${props.viewType}_view` : '' }}">
|
||||
<t t-component="components.ControlPanel" t-if="display.controlPanel">
|
||||
<!-- Empty body to assign slot id to control panel -->
|
||||
</t>
|
||||
<div class="o_content" t-att-class="{ o_component_with_search_panel: display.searchPanel }">
|
||||
<t t-component="components.Banner" t-if="components.Banner and display.banner" />
|
||||
<t t-component="components.SearchPanel" t-if="display.searchPanel" />
|
||||
<t t-slot="default" />
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -12,14 +12,14 @@ export class OnboardingBanner extends owl.Component {
|
||||
useActionLinks({
|
||||
resModel,
|
||||
reload: async () => {
|
||||
this.bannerHTML = await this.loadBanner(this.props.bannerRoute);
|
||||
this.bannerHTML = await this.loadBanner(this.env.config.bannerRoute);
|
||||
this.render();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async willStart() {
|
||||
this.bannerHTML = await this.loadBanner(this.props.bannerRoute);
|
||||
this.bannerHTML = await this.loadBanner(this.env.config.bannerRoute);
|
||||
}
|
||||
|
||||
async loadBanner(bannerRoute) {
|
||||
@@ -47,4 +47,6 @@ export class OnboardingBanner extends owl.Component {
|
||||
return new XMLSerializer().serializeToString(banner);
|
||||
}
|
||||
}
|
||||
|
||||
OnboardingBanner.template = owl.tags.xml`<div t-raw="bannerHTML" />`;
|
||||
OnboardingBanner.props = {};
|
||||
|
||||
@@ -4,11 +4,10 @@ import { _lt } from "@web/core/l10n/translation";
|
||||
import { download } from "@web/core/network/download";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { ControlPanel } from "@web/search/control_panel/control_panel";
|
||||
import { SearchPanel } from "@web/search/search_panel/search_panel";
|
||||
import { useModel } from "@web/views/helpers/model";
|
||||
import { standardViewProps } from "@web/views/helpers/standard_view_props";
|
||||
import { useSetupView } from "@web/views/helpers/view_hook";
|
||||
import { Layout } from "@web/views/layout";
|
||||
import { PivotArchParser } from "@web/views/pivot/pivot_arch_parser";
|
||||
import { PivotModel } from "@web/views/pivot/pivot_model";
|
||||
import { PivotRenderer } from "@web/views/pivot/pivot_renderer";
|
||||
@@ -130,8 +129,9 @@ export class PivotView extends Component {
|
||||
});
|
||||
|
||||
// retrieve form and list view ids from the action
|
||||
const { views = [] } = this.env.config;
|
||||
this.views = ["list", "form"].map((viewType) => {
|
||||
const view = this.props.info.views.find((view) => view[1] === viewType);
|
||||
const view = views.find((view) => view[1] === viewType);
|
||||
return [view ? view[0] : false, viewType];
|
||||
});
|
||||
|
||||
@@ -157,7 +157,7 @@ export class PivotView extends Component {
|
||||
|
||||
PivotView.template = "web.PivotView";
|
||||
PivotView.buttonTemplate = "web.PivotView.Buttons";
|
||||
PivotView.components = { ControlPanel, SearchPanel, Renderer: PivotRenderer };
|
||||
PivotView.components = { Renderer: PivotRenderer, Layout };
|
||||
|
||||
PivotView.props = {
|
||||
...standardViewProps,
|
||||
|
||||
@@ -17,30 +17,26 @@
|
||||
</t>
|
||||
|
||||
<t t-name="web.PivotView" owl="1">
|
||||
<div class="o_pivot_view" t-att-class="{o_view_sample_data: model.useSampleModel }">
|
||||
<ControlPanel t-if="env.searchModel.display.controlPanel" t-props="props.info">
|
||||
<t t-set-slot="control-panel-bottom-left">
|
||||
<t t-call="{{ constructor.buttonTemplate }}"/>
|
||||
<Layout viewType="'pivot'" useSampleModel="model.useSampleModel">
|
||||
<t t-set-slot="control-panel-bottom-left">
|
||||
<t t-call="{{ constructor.buttonTemplate }}"/>
|
||||
</t>
|
||||
<t t-set="displayNoContent" t-value="
|
||||
props.info.noContentHelp !== false and (
|
||||
!(model.hasData() and model.metaData.activeMeasures.length) or
|
||||
model.useSampleModel
|
||||
)"
|
||||
/>
|
||||
<t t-if="displayNoContent">
|
||||
<t t-if="props.info.noContentHelp" t-call="web.ActionHelper">
|
||||
<t t-set="noContentHelp" t-value="props.info.noContentHelp"/>
|
||||
</t>
|
||||
</ControlPanel>
|
||||
<div class="o_content">
|
||||
<SearchPanel t-if="env.searchModel.display.searchPanel" />
|
||||
<t t-set="displayNoContent" t-value="
|
||||
props.info.noContentHelp !== false and (
|
||||
!(model.hasData() and model.metaData.activeMeasures.length) or
|
||||
model.useSampleModel
|
||||
)" />
|
||||
<t t-if="displayNoContent">
|
||||
<t t-if="props.info.noContentHelp" t-call="web.ActionHelper">
|
||||
<t t-set="noContentHelp" t-value="props.info.noContentHelp"/>
|
||||
</t>
|
||||
<t t-else="" t-call="web.NoContentHelper"/>
|
||||
</t>
|
||||
<t t-if="model.hasData() and model.metaData.activeMeasures.length">
|
||||
<Renderer model="model" onCellClicked="cell => onOpenView(cell)"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<t t-else="" t-call="web.NoContentHelper"/>
|
||||
</t>
|
||||
<t t-if="model.hasData() and model.metaData.activeMeasures.length">
|
||||
<Renderer model="model" onCellClicked="cell => onOpenView(cell)"/>
|
||||
</t>
|
||||
</Layout>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
||||
@@ -2,21 +2,22 @@
|
||||
|
||||
import { evaluateExpr } from "@web/core/py_js/py";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { KeepLast } from "@web/core/utils/concurrency";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { deepCopy } from "@web/core/utils/objects";
|
||||
import { WithSearch } from "@web/search/with_search/with_search";
|
||||
import { useActionLinks } from "@web/views/helpers/view_hook";
|
||||
import { extractLayoutComponents } from "@web/views/layout";
|
||||
|
||||
const viewRegistry = registry.category("views");
|
||||
|
||||
const { Component } = owl;
|
||||
const { Component, hooks } = owl;
|
||||
const { useSubEnv } = hooks;
|
||||
|
||||
/** @typedef {Object} ViewProps
|
||||
* @property {string} resModel
|
||||
* @property {string} type
|
||||
*
|
||||
* @property {Array[]} [views]
|
||||
*
|
||||
* @property {string} [arch] if given, fields must be given too /\ no post processing is done (evaluation of "groups" attribute,...)
|
||||
* @property {Object} [fields] if given, arch must be given too
|
||||
* @property {number|false} [viewId]
|
||||
@@ -35,18 +36,11 @@ const { Component } = owl;
|
||||
* @property {string[]} [groupBy]
|
||||
* @property {string[]} [orderBy]
|
||||
*
|
||||
* @property {Object} [action={}]
|
||||
* @property {Object} [actionFlags={}]
|
||||
* @property {string} [displayName]
|
||||
*
|
||||
* @property {boolean} [useSampleModel]
|
||||
* @property {string} [noContentHelp]
|
||||
*
|
||||
* @property {Object} [display={}] to rework
|
||||
*
|
||||
* @property {Object[]} [breadcrumbs]
|
||||
* @property {Object[]} [viewSwitcherEntries]
|
||||
*
|
||||
* manipulated by withSearch
|
||||
*
|
||||
* @property {boolean} [activateFavorite]
|
||||
@@ -55,12 +49,10 @@ const { Component } = owl;
|
||||
* @property {Object} [globalState]
|
||||
*/
|
||||
|
||||
const STANDARD_PROPS = new Set([
|
||||
const STANDARD_PROPS = [
|
||||
"resModel",
|
||||
"type",
|
||||
|
||||
"views",
|
||||
|
||||
"arch",
|
||||
"fields",
|
||||
"viewId",
|
||||
@@ -79,17 +71,10 @@ const STANDARD_PROPS = new Set([
|
||||
"groupBy",
|
||||
"orderBy",
|
||||
|
||||
"action",
|
||||
"actionFlags",
|
||||
"displayName",
|
||||
|
||||
"useSampleModel",
|
||||
"noContentHelp",
|
||||
|
||||
"breadcrumbs",
|
||||
"display",
|
||||
"viewSwitcherEntries",
|
||||
|
||||
"globalState",
|
||||
|
||||
"activateFavorite",
|
||||
@@ -97,7 +82,7 @@ const STANDARD_PROPS = new Set([
|
||||
// LEGACY: remove this later (clean when mappings old state <-> new state are established)
|
||||
"searchPanel",
|
||||
"searchModel",
|
||||
]);
|
||||
];
|
||||
|
||||
export class View extends Component {
|
||||
setup() {
|
||||
@@ -117,11 +102,11 @@ export class View extends Component {
|
||||
}
|
||||
|
||||
this.viewService = useService("view");
|
||||
|
||||
this.withSearchProps = null;
|
||||
|
||||
owl.hooks.useSubEnv({
|
||||
useSubEnv({
|
||||
keepLast: new KeepLast(),
|
||||
config: { ...(this.env.config || {}) },
|
||||
});
|
||||
useActionLinks({ resModel });
|
||||
}
|
||||
@@ -132,16 +117,16 @@ export class View extends Component {
|
||||
const type = ViewClass.type;
|
||||
|
||||
// determine views for which descriptions should be obtained
|
||||
let { views, viewId, searchViewId } = this.props;
|
||||
let { viewId, searchViewId } = this.props;
|
||||
|
||||
views = JSON.parse(JSON.stringify(views));
|
||||
|
||||
const view = views.find((v) => v[1] === type);
|
||||
if (view) {
|
||||
const views = deepCopy(this.env.config.views || []);
|
||||
const view = views.find((v) => v[1] === type) || [];
|
||||
if (view.length) {
|
||||
view[0] = viewId !== undefined ? viewId : view[0];
|
||||
viewId = view[0];
|
||||
} else {
|
||||
views.push([viewId || false, type]); // viewId will remain undefined if not specified and loadView=false
|
||||
view.push(viewId || false, type);
|
||||
views.push(view); // viewId will remain undefined if not specified and loadView=false
|
||||
}
|
||||
|
||||
const searchView = views.find((v) => v[1] === "search");
|
||||
@@ -154,26 +139,27 @@ export class View extends Component {
|
||||
// searchViewId will remains undefined if loadSearchView=false
|
||||
|
||||
// prepare view description
|
||||
const { action, context, resModel, loadActionMenus, loadIrFilters } = this.props;
|
||||
const { context, resModel, loadActionMenus, loadIrFilters } = this.props;
|
||||
let { arch, fields, searchViewArch, searchViewFields, irFilters, actionMenus } = this.props;
|
||||
|
||||
let loadView = !arch || (!actionMenus && loadActionMenus);
|
||||
let loadSearchView =
|
||||
(searchViewId !== undefined && !searchViewArch) || (!irFilters && loadIrFilters);
|
||||
|
||||
let viewDescription = { resModel, type };
|
||||
let viewDescription = { viewId, resModel, type };
|
||||
let searchViewDescription;
|
||||
if (loadView || loadSearchView) {
|
||||
// view description (or search view description if required) is incomplete
|
||||
// a loadViews is done to complete the missing information
|
||||
const viewDescriptions = await this.viewService.loadViews(
|
||||
{ context, resModel, views },
|
||||
{ actionId: action.id, loadActionMenus, loadIrFilters }
|
||||
{ actionId: this.env.config.actionId, loadActionMenus, loadIrFilters }
|
||||
);
|
||||
// Note: if this.props.views is different from views, the cached descriptions
|
||||
// will certainly not be reused! (but for the standard flow this will work as
|
||||
// before)
|
||||
viewDescription = viewDescriptions[type];
|
||||
view[0] = viewDescription.viewId;
|
||||
searchViewDescription = viewDescriptions.search;
|
||||
if (loadSearchView) {
|
||||
if (!searchViewArch) {
|
||||
@@ -184,6 +170,7 @@ export class View extends Component {
|
||||
irFilters = searchViewDescription.irFilters;
|
||||
}
|
||||
}
|
||||
this.env.config.views = views;
|
||||
}
|
||||
|
||||
if (!arch) {
|
||||
@@ -197,33 +184,28 @@ export class View extends Component {
|
||||
const parser = new DOMParser();
|
||||
const xml = parser.parseFromString(arch, "text/xml");
|
||||
const rootNode = xml.documentElement;
|
||||
const rootAttrs = {};
|
||||
for (const attrName of rootNode.getAttributeNames()) {
|
||||
rootAttrs[attrName] = rootNode.getAttribute(attrName);
|
||||
}
|
||||
|
||||
const subType = rootNode.getAttribute("js_class");
|
||||
const bannerRoute = rootNode.getAttribute("banner_route");
|
||||
const sample = rootNode.getAttribute("sample");
|
||||
|
||||
// determine ViewClass to instantiate (if not already done)
|
||||
if (rootAttrs.js_class) {
|
||||
ViewClass = viewRegistry.get(rootAttrs.js_class);
|
||||
if (subType) {
|
||||
ViewClass = viewRegistry.get(subType);
|
||||
}
|
||||
|
||||
const bannerRoute = rootAttrs.banner_route;
|
||||
Object.assign(this.env.config, {
|
||||
viewId: viewDescription.viewId,
|
||||
viewType: type,
|
||||
viewSubType: subType,
|
||||
bannerRoute,
|
||||
...extractLayoutComponents(ViewClass),
|
||||
});
|
||||
|
||||
// prepare the view props
|
||||
let viewProps = {
|
||||
info: {
|
||||
actionFlags: this.props.actionFlags,
|
||||
action: this.props.action,
|
||||
viewId: viewDescription.viewId,
|
||||
views,
|
||||
mode: this.props.display.mode,
|
||||
actionMenus,
|
||||
breadcrumbs: this.props.breadcrumbs,
|
||||
viewSwitcherEntries: this.props.viewSwitcherEntries,
|
||||
displayName: this.props.displayName,
|
||||
},
|
||||
const viewProps = {
|
||||
info: { actionMenus, mode: this.props.display.mode },
|
||||
arch,
|
||||
bannerRoute,
|
||||
fields,
|
||||
resModel,
|
||||
useSampleModel: false,
|
||||
@@ -234,12 +216,12 @@ export class View extends Component {
|
||||
|
||||
if ("useSampleModel" in this.props) {
|
||||
viewProps.useSampleModel = this.props.useSampleModel;
|
||||
} else if (rootAttrs.sample) {
|
||||
viewProps.useSampleModel = Boolean(evaluateExpr(rootAttrs.sample));
|
||||
} else if (sample) {
|
||||
viewProps.useSampleModel = Boolean(evaluateExpr(sample));
|
||||
}
|
||||
|
||||
for (const key in this.props) {
|
||||
if (!STANDARD_PROPS.has(key)) {
|
||||
if (!STANDARD_PROPS.includes(key)) {
|
||||
viewProps[key] = this.props[key];
|
||||
}
|
||||
}
|
||||
@@ -254,13 +236,11 @@ export class View extends Component {
|
||||
}
|
||||
|
||||
// prepare the WithSearh component props
|
||||
this.withSearchProps = { ...this.props };
|
||||
|
||||
Object.assign(this.withSearchProps, {
|
||||
this.withSearchProps = {
|
||||
...this.props,
|
||||
Component: ViewClass,
|
||||
componentProps: viewProps,
|
||||
view: { id: viewId, type },
|
||||
});
|
||||
};
|
||||
|
||||
if (searchViewId !== undefined) {
|
||||
this.withSearchProps.searchViewId = searchViewId;
|
||||
@@ -298,13 +278,10 @@ export class View extends Component {
|
||||
View.template = "web.View";
|
||||
View.components = { WithSearch };
|
||||
View.defaultProps = {
|
||||
action: { id: false, views: [] },
|
||||
display: {},
|
||||
context: {},
|
||||
actionFlags: {},
|
||||
loadActionMenus: false,
|
||||
loadIrFilters: false,
|
||||
views: [],
|
||||
};
|
||||
|
||||
View.searchMenuTypes = ["filter", "groupBy", "favorite"];
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { cleanDomFromBootstrap } from "@web/legacy/utils";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { useBus } from "@web/core/utils/hooks";
|
||||
import { makeContext } from "@web/core/context";
|
||||
import { useDebugCategory } from "@web/core/debug/debug_context";
|
||||
import { download } from "@web/core/network/download";
|
||||
import { evaluateExpr } from "@web/core/py_js/py";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { KeepLast } from "@web/core/utils/concurrency";
|
||||
import { useBus } from "@web/core/utils/hooks";
|
||||
import { sprintf } from "@web/core/utils/strings";
|
||||
import { useDebugCategory } from "@web/core/debug/debug_context";
|
||||
import { cleanDomFromBootstrap } from "@web/legacy/utils";
|
||||
import { View } from "@web/views/view";
|
||||
import { ActionDialog } from "./action_dialog";
|
||||
import { CallbackRecorder } from "./action_hook";
|
||||
import { View } from "@web/views/view";
|
||||
|
||||
const { Component, hooks, tags } = owl;
|
||||
const { useRef, useSubEnv } = hooks;
|
||||
@@ -307,10 +307,19 @@ function makeActionManager(env) {
|
||||
/**
|
||||
* @param {ClientAction | ActWindowAction} action
|
||||
* @param {Object} props
|
||||
* @returns {ActionProps}
|
||||
* @returns {{ props: ActionProps, config: Config }}
|
||||
*/
|
||||
function _getActionProps(action, props) {
|
||||
return Object.assign({}, props, { action, actionId: action.id });
|
||||
function _getActionInfo(action, props) {
|
||||
return {
|
||||
props: Object.assign({}, props, { action, actionId: action.id }),
|
||||
config: {
|
||||
actionId: action.id,
|
||||
actionType: action.type,
|
||||
actionFlags: action.flags,
|
||||
displayName: action.display_name || action.name || "",
|
||||
views: action.views,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -366,9 +375,9 @@ function makeActionManager(env) {
|
||||
* @param {ActWindowAction} action
|
||||
* @param {BaseView[]} views
|
||||
* @param {Object} props
|
||||
* @returns {ViewProps}
|
||||
* @returns {{ props: ViewProps, config: Config }}
|
||||
*/
|
||||
function _getViewProps(view, action, views, props = {}) {
|
||||
function _getViewInfo(view, action, views, props = {}) {
|
||||
const target = action.target;
|
||||
const viewSwitcherEntries = views
|
||||
.filter((v) => v.multiRecord === view.multiRecord)
|
||||
@@ -385,25 +394,15 @@ function makeActionManager(env) {
|
||||
return viewSwitcherEntry;
|
||||
});
|
||||
const context = action.context || {};
|
||||
const flags = action.flags || {};
|
||||
const viewProps = Object.assign({}, props, {
|
||||
actionFlags: Object.assign({}, flags, flags[view.type]), // review system
|
||||
action: {
|
||||
id: action.id || false,
|
||||
type: action.type || false,
|
||||
views: action.views || [],
|
||||
},
|
||||
context,
|
||||
display: { mode: target === "new" ? "inDialog" : target },
|
||||
displayName: action.display_name || action.name,
|
||||
domain: action.domain || [],
|
||||
groupBy: action.context.group_by || [],
|
||||
loadActionMenus: target !== "new" && target !== "inline",
|
||||
loadIrFilters: action.views.some((v) => v[1] === "search"),
|
||||
resModel: action.res_model,
|
||||
type: view.type,
|
||||
views: action.views,
|
||||
viewSwitcherEntries,
|
||||
});
|
||||
|
||||
if (target === "inline") {
|
||||
@@ -433,11 +432,25 @@ function makeActionManager(env) {
|
||||
// LEGACY CODE COMPATIBILITY: remove when all views will be written in owl
|
||||
if (view.isLegacy) {
|
||||
const legacyActionInfo = { ...action, ...viewProps.action };
|
||||
Object.assign(viewProps, { action: legacyActionInfo, View: view });
|
||||
Object.assign(viewProps, {
|
||||
action: legacyActionInfo,
|
||||
View: view,
|
||||
views: action.views,
|
||||
});
|
||||
}
|
||||
// END LEGACY CODE COMPATIBILITY
|
||||
|
||||
return viewProps;
|
||||
return {
|
||||
props: viewProps,
|
||||
config: {
|
||||
actionId: action.id,
|
||||
actionType: action.type,
|
||||
actionFlags: action.flags,
|
||||
displayName: action.display_name || action.name || "",
|
||||
views: action.views,
|
||||
viewSwitcherEntries,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -502,11 +515,24 @@ function makeActionManager(env) {
|
||||
});
|
||||
const action = controller.action;
|
||||
|
||||
// Compute breadcrumbs
|
||||
const index = _computeStackIndex(options);
|
||||
const controllerArray = [controller];
|
||||
if (options.lazyController) {
|
||||
controllerArray.unshift(options.lazyController);
|
||||
}
|
||||
const nextStack = controllerStack.slice(0, index).concat(controllerArray);
|
||||
controller.config.breadcrumbs = _getBreadcrumbs(nextStack.slice(0, -1));
|
||||
if (controller.Component.isLegacy) {
|
||||
controller.props.breadcrumbs = controller.config.breadcrumbs;
|
||||
}
|
||||
|
||||
class ControllerComponent extends Component {
|
||||
setup() {
|
||||
this.Component = controller.Component;
|
||||
this.componentRef = useRef("component");
|
||||
useDebugCategory("action", { action });
|
||||
useSubEnv({ config: controller.config });
|
||||
if (action.target !== "new") {
|
||||
this.__beforeLeave__ = new CallbackRecorder();
|
||||
this.__getGlobalState__ = new CallbackRecorder();
|
||||
@@ -679,13 +705,6 @@ function makeActionManager(env) {
|
||||
controller.props.globalState = controller.action.globalState;
|
||||
}
|
||||
|
||||
const index = _computeStackIndex(options);
|
||||
const controllerArray = [controller];
|
||||
if (options.lazyController) {
|
||||
controllerArray.unshift(options.lazyController);
|
||||
}
|
||||
const nextStack = controllerStack.slice(0, index).concat(controllerArray);
|
||||
controller.props.breadcrumbs = _getBreadcrumbs(nextStack.slice(0, nextStack.length - 1));
|
||||
const closingProm = _executeCloseAction();
|
||||
|
||||
controller.__info__ = {
|
||||
@@ -772,7 +791,7 @@ function makeActionManager(env) {
|
||||
action,
|
||||
view,
|
||||
views,
|
||||
props: _getViewProps(view, action, views, options.props),
|
||||
..._getViewInfo(view, action, views, options.props),
|
||||
};
|
||||
action.controllers[view.type] = controller;
|
||||
|
||||
@@ -789,7 +808,7 @@ function makeActionManager(env) {
|
||||
action,
|
||||
view: lazyView,
|
||||
views,
|
||||
props: _getViewProps(lazyView, action, views),
|
||||
..._getViewInfo(lazyView, action, views),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -829,7 +848,7 @@ function makeActionManager(env) {
|
||||
jsId: `controller_${++id}`,
|
||||
Component: clientAction,
|
||||
action,
|
||||
props: _getActionProps(action, options.props),
|
||||
..._getActionInfo(action, options.props),
|
||||
};
|
||||
return _updateUI(controller, {
|
||||
clearBreadcrumbs: options.clearBreadcrumbs,
|
||||
@@ -1197,7 +1216,10 @@ function makeActionManager(env) {
|
||||
}
|
||||
// END LEGACY CODE COMPATIBILITY
|
||||
|
||||
newController.props = _getViewProps(view, controller.action, controller.views, props);
|
||||
Object.assign(
|
||||
newController,
|
||||
_getViewInfo(view, controller.action, controller.views, props)
|
||||
);
|
||||
controller.action.controllers[viewType] = newController;
|
||||
let index;
|
||||
if (view.multiRecord) {
|
||||
@@ -1235,7 +1257,10 @@ function makeActionManager(env) {
|
||||
}
|
||||
const controller = controllerStack[index];
|
||||
if (controller.action.type === "ir.actions.act_window") {
|
||||
controller.props = _getViewProps(controller.view, controller.action, controller.views);
|
||||
Object.assign(
|
||||
controller,
|
||||
_getViewInfo(controller.view, controller.action, controller.views)
|
||||
);
|
||||
}
|
||||
await clearUncommittedChanges(env);
|
||||
return _updateUI(controller, { index });
|
||||
|
||||
@@ -110,6 +110,7 @@ export async function makeTestEnv(config = {}) {
|
||||
});
|
||||
|
||||
const env = makeEnv();
|
||||
env.config = config.config || {};
|
||||
await startServices(env);
|
||||
env.qweb.addTemplates(window.__ODOO_TEMPLATES__);
|
||||
return env;
|
||||
|
||||
@@ -26,6 +26,7 @@ import legacyViewRegistry from "web.view_registry";
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
const viewRegistry = registry.category("views");
|
||||
const searchModelRegistry = registry.category("search_models");
|
||||
|
||||
let serverData;
|
||||
QUnit.module("Views", (hooks) => {
|
||||
@@ -81,7 +82,7 @@ QUnit.module("Views", (hooks) => {
|
||||
ToyView.searchMenuTypes = ["filter", "groupBy", "comparison", "favorite"];
|
||||
ToyView.template = owl.tags.xml`
|
||||
<div class="o_toy_view">
|
||||
<ControlPanel t-props="props.info"/>
|
||||
<ControlPanel />
|
||||
</div>
|
||||
`;
|
||||
ToyView.type = "toy";
|
||||
|
||||
@@ -57,7 +57,7 @@ QUnit.module("Search", (hooks) => {
|
||||
serverData,
|
||||
resModel: "foo",
|
||||
Component: ControlPanel,
|
||||
componentProps: {
|
||||
config: {
|
||||
breadcrumbs: [{ jsId: "controller_7", name: "Previous" }],
|
||||
displayName: "Current",
|
||||
},
|
||||
@@ -83,7 +83,7 @@ QUnit.module("Search", (hooks) => {
|
||||
serverData,
|
||||
resModel: "foo",
|
||||
Component: ControlPanel,
|
||||
componentProps: {
|
||||
config: {
|
||||
viewSwitcherEntries: [
|
||||
{ type: "list", active: true, icon: "fa-list-ul", name: "List" },
|
||||
{ type: "kanban", icon: "fa-th-large", name: "Kanban" },
|
||||
|
||||
@@ -75,7 +75,9 @@ QUnit.module("Search", (hooks) => {
|
||||
Component: ControlPanel,
|
||||
searchMenuTypes: ["favorite"],
|
||||
searchViewId: false,
|
||||
displayName: "Action Name",
|
||||
config: {
|
||||
displayName: "Action Name",
|
||||
},
|
||||
});
|
||||
|
||||
await toggleFavoriteMenu(controlPanel);
|
||||
@@ -288,7 +290,7 @@ QUnit.module("Search", (hooks) => {
|
||||
if (args.model === "ir.filters" && args.method === "create_or_replace") {
|
||||
const irFilter = args.args[0];
|
||||
assert.deepEqual(irFilter, {
|
||||
action_id: false,
|
||||
action_id: undefined,
|
||||
context: { group_by: [] },
|
||||
domain: "[]",
|
||||
is_default: false,
|
||||
|
||||
@@ -67,7 +67,9 @@ QUnit.module("Search", (hooks) => {
|
||||
Component: ControlPanel,
|
||||
searchMenuTypes: ["favorite"],
|
||||
searchViewId: false,
|
||||
displayName: "Action Name",
|
||||
config: {
|
||||
displayName: "Action Name",
|
||||
},
|
||||
});
|
||||
|
||||
assert.containsOnce(controlPanel, "div.o_favorite_menu > button i.fa.fa-star");
|
||||
@@ -98,7 +100,9 @@ QUnit.module("Search", (hooks) => {
|
||||
Component: ControlPanel,
|
||||
searchMenuTypes: ["favorite"],
|
||||
searchViewId: false,
|
||||
displayName: "Action Name",
|
||||
config: {
|
||||
displayName: "Action Name",
|
||||
},
|
||||
});
|
||||
|
||||
assert.containsOnce(controlPanel, "div.o_favorite_menu > button i.fa.fa-star");
|
||||
|
||||
@@ -4,10 +4,10 @@ import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
|
||||
import { notificationService } from "@web/core/notifications/notification_service";
|
||||
import { ormService } from "@web/core/orm_service";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { CustomFavoriteItem } from "@web/search/favorite_menu/custom_favorite_item";
|
||||
import { WithSearch } from "@web/search/with_search/with_search";
|
||||
import { viewService } from "@web/views/view_service";
|
||||
import { actionService } from "@web/webclient/actions/action_service";
|
||||
import { CustomFavoriteItem } from "@web/search/favorite_menu/custom_favorite_item";
|
||||
import { registerCleanup } from "../helpers/cleanup";
|
||||
import { makeTestEnv } from "../helpers/mock_env";
|
||||
import { click, getFixture, triggerEvent } from "../helpers/utils";
|
||||
@@ -34,16 +34,25 @@ export const setupControlPanelFavoriteMenuRegistry = () => {
|
||||
};
|
||||
|
||||
export const makeWithSearch = async (params) => {
|
||||
const serverData = params.serverData || undefined;
|
||||
const mockRPC = params.mockRPC || undefined;
|
||||
const props = { ...params };
|
||||
|
||||
const serverData = props.serverData || undefined;
|
||||
const mockRPC = props.mockRPC || undefined;
|
||||
const config = props.config || {};
|
||||
|
||||
delete props.serverData;
|
||||
delete props.mockRPC;
|
||||
const env = await makeTestEnv({ serverData, mockRPC });
|
||||
delete props.config;
|
||||
|
||||
const env = await makeTestEnv({ serverData, mockRPC, config });
|
||||
|
||||
const target = getFixture();
|
||||
const withSearch = await mount(WithSearch, { env, props, target });
|
||||
|
||||
registerCleanup(() => withSearch.destroy());
|
||||
|
||||
const component = Object.values(withSearch.__owl__.children)[0];
|
||||
|
||||
return component;
|
||||
};
|
||||
|
||||
|
||||
@@ -401,7 +401,7 @@ QUnit.module("Search", (hooks) => {
|
||||
<searchpanel/>
|
||||
</search>
|
||||
`,
|
||||
view: { type: "kanban" },
|
||||
config: { viewType: "kanban" },
|
||||
});
|
||||
assert.deepEqual(model.getSections(), []);
|
||||
});
|
||||
@@ -418,7 +418,7 @@ QUnit.module("Search", (hooks) => {
|
||||
</search>
|
||||
`,
|
||||
resModel: "partner",
|
||||
view: { type: "kanban" },
|
||||
config: { viewType: "kanban" },
|
||||
});
|
||||
const sections = model.getSections();
|
||||
for (const section of sections) {
|
||||
@@ -488,7 +488,7 @@ QUnit.module("Search", (hooks) => {
|
||||
</search>
|
||||
`,
|
||||
resModel: "partner",
|
||||
view: { type: "kanban" },
|
||||
config: { viewType: "kanban" },
|
||||
});
|
||||
const sections = model.getSections();
|
||||
for (const section of sections) {
|
||||
|
||||
@@ -101,7 +101,7 @@ const makeTestComponent = ({ onWillStart, onWillUpdateProps } = {}) => {
|
||||
TestComponent.components = { FilterMenu, GroupByMenu, SearchPanel };
|
||||
TestComponent.template = xml`
|
||||
<div class="o_test_component">
|
||||
<SearchPanel />
|
||||
<SearchPanel t-if="env.searchModel.display.searchPanel" />
|
||||
<FilterMenu />
|
||||
<GroupByMenu />
|
||||
</div>`;
|
||||
@@ -282,7 +282,29 @@ QUnit.module("Search", (hooks) => {
|
||||
searchViewId: false,
|
||||
display: { searchPanel: false },
|
||||
});
|
||||
assert.doesNotHaveClass(comp.el, "o_component_with_search_panel");
|
||||
assert.containsNone(comp, ".o_search_panel");
|
||||
assert.deepEqual(getDomain(), []); // initial domain
|
||||
});
|
||||
|
||||
QUnit.test("basic rendering of a component with empty search panel", async (assert) => {
|
||||
assert.expect(2);
|
||||
|
||||
serverData.views["partner,false,search"] = `<search><searchpanel /></search>`;
|
||||
|
||||
const { TestComponent, getDomain } = makeTestComponent();
|
||||
const comp = await makeWithSearch({
|
||||
serverData,
|
||||
async mockRPC(route, { method, model }) {
|
||||
if (/search_panel_/.test(method || route)) {
|
||||
assert.step(`${method || route} on ${model}`);
|
||||
}
|
||||
},
|
||||
Component: TestComponent,
|
||||
resModel: "partner",
|
||||
searchViewId: false,
|
||||
});
|
||||
|
||||
assert.containsNone(comp, ".o_search_panel");
|
||||
assert.deepEqual(getDomain(), []); // initial domain
|
||||
});
|
||||
|
||||
@@ -299,7 +321,6 @@ QUnit.module("Search", (hooks) => {
|
||||
Component: TestComponent,
|
||||
resModel: "partner",
|
||||
searchViewId: false,
|
||||
view: { type: "kanban" },
|
||||
});
|
||||
|
||||
assert.containsOnce(comp, ".o_search_panel");
|
||||
@@ -361,7 +382,7 @@ QUnit.module("Search", (hooks) => {
|
||||
Component: TestComponent,
|
||||
resModel: "partner",
|
||||
searchViewId: false,
|
||||
view: { type: "toy" },
|
||||
config: { viewType: "toy" },
|
||||
});
|
||||
|
||||
const sectionHeaderIcons = comp.el.querySelectorAll(".o_search_panel_section_header i");
|
||||
@@ -397,7 +418,7 @@ QUnit.module("Search", (hooks) => {
|
||||
Component: TestComponent,
|
||||
resModel: "partner",
|
||||
searchViewId: false,
|
||||
view: { type: "kanban" },
|
||||
config: { viewType: "kanban" },
|
||||
});
|
||||
|
||||
assert.containsOnce(comp, ".o_search_panel_section");
|
||||
@@ -422,7 +443,7 @@ QUnit.module("Search", (hooks) => {
|
||||
Component: TestComponent,
|
||||
resModel: "partner",
|
||||
searchViewId: false,
|
||||
view: { type: "kanban" },
|
||||
config: { viewType: "kanban" },
|
||||
});
|
||||
|
||||
const headers = comp.el.getElementsByClassName("o_search_panel_section_header");
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { BORDER_WHITE, DEFAULT_BG } from "@web/views/graph/colors";
|
||||
import { dialogService } from "@web/core/dialog/dialog_service";
|
||||
import { GraphArchParser } from "@web/views/graph/graph_arch_parser";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { makeView } from "./helpers";
|
||||
import { click, makeDeferred, nextTick, triggerEvent } from "@web/../tests/helpers/utils";
|
||||
import { makeFakeLocalizationService } from "@web/../tests/helpers/mock_services";
|
||||
import { createWebClient, doAction } from "@web/../tests/webclient/helpers";
|
||||
import { click, makeDeferred, nextTick, triggerEvent } from "@web/../tests/helpers/utils";
|
||||
import {
|
||||
editFavoriteName,
|
||||
saveFavorite,
|
||||
@@ -22,6 +16,12 @@ import {
|
||||
toggleMenuItemOption,
|
||||
toggleSaveFavorite,
|
||||
} from "@web/../tests/search/helpers";
|
||||
import { makeView } from "@web/../tests/views/helpers";
|
||||
import { createWebClient, doAction } from "@web/../tests/webclient/helpers";
|
||||
import { dialogService } from "@web/core/dialog/dialog_service";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { BORDER_WHITE, DEFAULT_BG } from "@web/views/graph/colors";
|
||||
import { GraphArchParser } from "@web/views/graph/graph_arch_parser";
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
@@ -30,7 +30,8 @@ function getGraphModelMetaData(graph) {
|
||||
}
|
||||
|
||||
export function getGraphRenderer(graph) {
|
||||
return Object.values(graph.__owl__.children).find((c) => c.chart);
|
||||
const layout = Object.values(graph.__owl__.children)[0];
|
||||
return Object.values(layout.__owl__.children).find((c) => c.chart);
|
||||
}
|
||||
|
||||
function getChart(graph) {
|
||||
@@ -2031,26 +2032,9 @@ QUnit.module("Views", (hooks) => {
|
||||
type: "graph",
|
||||
resModel: "foo",
|
||||
noContentHelp: '<p class="abc">This helper should not be displayed in graph views</p>',
|
||||
views: [[false, "search"]],
|
||||
});
|
||||
assert.containsOnce(graph, "div.o_graph_canvas_container canvas");
|
||||
assert.containsNone(graph, "div.o_view_nocontent");
|
||||
assert.containsNone(graph, ".abc");
|
||||
await toggleFilterMenu(graph);
|
||||
await toggleMenuItem(graph, "False Domain");
|
||||
assert.containsOnce(graph, "div.o_graph_canvas_container canvas");
|
||||
assert.containsNone(graph, "div.o_view_nocontent");
|
||||
assert.containsNone(graph, ".abc");
|
||||
});
|
||||
|
||||
QUnit.test("no content helper after update", async function (assert) {
|
||||
assert.expect(6);
|
||||
const graph = await makeView({
|
||||
serverData,
|
||||
type: "graph",
|
||||
resModel: "foo",
|
||||
noContentHelp: '<p class="abc">This helper should not be displayed in graph views</p>',
|
||||
views: [[false, "search"]],
|
||||
config: {
|
||||
views: [[false, "search"]],
|
||||
},
|
||||
});
|
||||
assert.containsOnce(graph, "div.o_graph_canvas_container canvas");
|
||||
assert.containsNone(graph, "div.o_view_nocontent");
|
||||
@@ -2719,7 +2703,9 @@ QUnit.module("Views", (hooks) => {
|
||||
resModel: "foo",
|
||||
groupBy: ["date:year", "product_id", "date", "date:quarter"],
|
||||
arch: `<graph type="line"/>`,
|
||||
views: [[false, "search"]],
|
||||
config: {
|
||||
views: [[false, "search"]],
|
||||
},
|
||||
});
|
||||
checkLabels(assert, graph, ["January 2016", "March 2016", "May 2016", "April 2016"]);
|
||||
// mockReadGroup does not always sort groups -> May 2016 is before April 2016 for that reason.
|
||||
@@ -2748,7 +2734,9 @@ QUnit.module("Views", (hooks) => {
|
||||
serverData,
|
||||
type: "graph",
|
||||
resModel: "foo",
|
||||
displayName: "Glou glou",
|
||||
config: {
|
||||
displayName: "Glou glou",
|
||||
},
|
||||
});
|
||||
assert.strictEqual(
|
||||
graph.el.querySelector(".o_control_panel .breadcrumb-item.active").innerText,
|
||||
@@ -2851,10 +2839,12 @@ QUnit.module("Views", (hooks) => {
|
||||
<field name="bar"/>
|
||||
</graph>
|
||||
`,
|
||||
views: [
|
||||
[364, "list"],
|
||||
[29, "form"],
|
||||
],
|
||||
config: {
|
||||
views: [
|
||||
[364, "list"],
|
||||
[29, "form"],
|
||||
],
|
||||
},
|
||||
});
|
||||
checkModeIs(assert, graph, "pie");
|
||||
checkDatasets(assert, graph, ["domains"], {
|
||||
@@ -3093,7 +3083,7 @@ QUnit.module("Views", (hooks) => {
|
||||
type: "graph",
|
||||
resModel: "foo",
|
||||
arch: `
|
||||
<graph>
|
||||
<graph>
|
||||
<field name="date"/>
|
||||
<field name="product_id"/>
|
||||
</graph>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { View } from "@web/views/view";
|
||||
import { getFixture } from "@web/../tests/helpers/utils";
|
||||
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
|
||||
import { registerCleanup } from "@web/../tests/helpers/cleanup";
|
||||
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
|
||||
import { getFixture } from "@web/../tests/helpers/utils";
|
||||
import { View } from "@web/views/view";
|
||||
import { _fieldsViewGet } from "../helpers/mock_server";
|
||||
import { addLegacyMockEnvironment } from "../webclient/helpers";
|
||||
|
||||
@@ -21,20 +21,26 @@ const { mount } = owl;
|
||||
|
||||
/**
|
||||
* @param {MakeViewParams} params
|
||||
* @param {Object} [options={}]
|
||||
* @param {boolean} [options.noFields] Do not add default fields
|
||||
* @returns {owl.Component}
|
||||
*/
|
||||
export async function makeView(params) {
|
||||
const serverData = params.serverData;
|
||||
const mockRPC = params.mockRPC;
|
||||
const legacyParams = params.legacyParams || {};
|
||||
const props = Object.assign({}, params);
|
||||
export const makeView = async (params, options = {}) => {
|
||||
const props = { ...params };
|
||||
const serverData = props.serverData;
|
||||
const mockRPC = props.mockRPC;
|
||||
const config = props.config || {};
|
||||
const legacyParams = props.legacyParams || {};
|
||||
|
||||
delete props.serverData;
|
||||
delete props.mockRPC;
|
||||
delete props.legacyParams;
|
||||
delete props.config;
|
||||
|
||||
const env = await makeTestEnv({ serverData, mockRPC });
|
||||
const defaultFields = serverData.models[props.resModel].fields;
|
||||
if (props.arch) {
|
||||
const env = await makeTestEnv({ serverData, mockRPC, config });
|
||||
|
||||
if (!options.noFields && props.arch) {
|
||||
const defaultFields = serverData.models[props.resModel].fields;
|
||||
if (!props.fields) {
|
||||
props.fields = Object.assign({}, defaultFields);
|
||||
// write the field name inside the field description (as done by fields_get)
|
||||
@@ -81,4 +87,4 @@ export async function makeView(params) {
|
||||
const concreteView = Object.values(withSearch.__owl__.children)[0];
|
||||
|
||||
return concreteView;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { getFixture } from "@web/../tests/helpers/utils";
|
||||
import { makeWithSearch, setupControlPanelServiceRegistry } from "@web/../tests/search/helpers";
|
||||
import { dialogService } from "@web/core/dialog/dialog_service";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { Layout } from "@web/views/layout";
|
||||
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
|
||||
|
||||
const { Component, hooks, mount, tags } = owl;
|
||||
const { xml } = tags;
|
||||
const { useSubEnv } = hooks;
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
let serverData;
|
||||
|
||||
QUnit.module("Views", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
serverData = {
|
||||
models: {
|
||||
foo: {
|
||||
fields: {
|
||||
aaa: {
|
||||
type: "selection",
|
||||
selection: [
|
||||
["a", "A"],
|
||||
["b", "B"],
|
||||
],
|
||||
},
|
||||
},
|
||||
records: [],
|
||||
},
|
||||
},
|
||||
views: {
|
||||
"foo,false,search": /* xml */ `
|
||||
<search>
|
||||
<searchpanel>
|
||||
<field name="aaa" />
|
||||
</searchpanel>
|
||||
</search>`,
|
||||
},
|
||||
};
|
||||
|
||||
setupControlPanelServiceRegistry();
|
||||
serviceRegistry.add("dialog", dialogService);
|
||||
});
|
||||
|
||||
QUnit.module("Layout");
|
||||
|
||||
QUnit.test("Simple rendering", async (assert) => {
|
||||
assert.expect(5);
|
||||
|
||||
class ToyComponent extends Component {}
|
||||
ToyComponent.template = xml`
|
||||
<Layout viewType="'toy'" useSampleModel="true">
|
||||
<div class="toy_content" />
|
||||
</Layout>`;
|
||||
ToyComponent.components = { Layout };
|
||||
|
||||
const env = await makeTestEnv({ config: {} });
|
||||
const comp = await mount(ToyComponent, { target: getFixture(), env });
|
||||
|
||||
assert.hasClass(comp.el, "o_toy_view o_view_sample_data");
|
||||
assert.containsNone(comp, ".o_control_panel");
|
||||
assert.containsNone(comp, ".o_component_with_search_panel");
|
||||
assert.containsNone(comp, ".o_search_panel");
|
||||
assert.containsOnce(comp, ".o_content > .toy_content");
|
||||
});
|
||||
|
||||
QUnit.test("Simple rendering: with search", async (assert) => {
|
||||
assert.expect(6);
|
||||
|
||||
class ToyComponent extends Component {}
|
||||
ToyComponent.template = xml`
|
||||
<Layout viewType="'toy'">
|
||||
<t t-set-slot="control-panel-top-right">
|
||||
<div class="toy_search_bar" />
|
||||
</t>
|
||||
<div class="toy_content" />
|
||||
</Layout>`;
|
||||
ToyComponent.components = { Layout };
|
||||
|
||||
const root = await makeWithSearch({
|
||||
serverData,
|
||||
Component: ToyComponent,
|
||||
resModel: "foo",
|
||||
searchViewId: false,
|
||||
});
|
||||
|
||||
assert.hasClass(root.el, "o_toy_view");
|
||||
assert.doesNotHaveClass(root.el, "o_view_sample_data");
|
||||
assert.containsOnce(root, ".o_control_panel .o_cp_top_right .toy_search_bar");
|
||||
assert.containsOnce(root, ".o_component_with_search_panel .o_search_panel");
|
||||
assert.containsNone(root, ".o_cp_searchview");
|
||||
assert.containsOnce(root, ".o_content > .toy_content");
|
||||
});
|
||||
|
||||
QUnit.test("Nested layouts", async (assert) => {
|
||||
assert.expect(10);
|
||||
|
||||
// Component C: bottom (no control panel)
|
||||
|
||||
class ToyC extends Component {
|
||||
setup() {
|
||||
useSubEnv({
|
||||
searchModel: {
|
||||
display: {
|
||||
controlPanel: false,
|
||||
searchPanel: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
ToyC.template = xml`
|
||||
<Layout viewType="'toy_c'">
|
||||
<div class="toy_c_content" />
|
||||
</Layout>`;
|
||||
ToyC.components = { Layout };
|
||||
|
||||
// Component B: center (with custom search panel)
|
||||
|
||||
class SearchPanel extends Component {}
|
||||
SearchPanel.template = xml`<div class="o_toy_search_panel" />`;
|
||||
|
||||
class ToyB extends Component {
|
||||
setup() {
|
||||
useSubEnv({ config: { SearchPanel } });
|
||||
}
|
||||
}
|
||||
ToyB.template = xml`
|
||||
<Layout viewType="'toy_b'">
|
||||
<t t-set-slot="control-panel-top-right">
|
||||
<div class="toy_b_breadcrumbs" />
|
||||
</t>
|
||||
<ToyC />
|
||||
</Layout>`;
|
||||
ToyB.components = { Layout, ToyC };
|
||||
|
||||
// Component A: top
|
||||
|
||||
class ToyA extends Component {}
|
||||
ToyA.template = xml`
|
||||
<Layout viewType="'toy_a'">
|
||||
<t t-set-slot="control-panel-top-right">
|
||||
<div class="toy_a_search" />
|
||||
</t>
|
||||
<ToyB />
|
||||
</Layout>`;
|
||||
ToyA.components = { Layout, ToyB };
|
||||
|
||||
const root = await makeWithSearch({
|
||||
serverData,
|
||||
Component: ToyA,
|
||||
resModel: "foo",
|
||||
searchViewId: false,
|
||||
});
|
||||
|
||||
assert.hasClass(root.el, "o_toy_a_view");
|
||||
assert.doesNotHaveClass(root.el, "o_view_sample_data");
|
||||
assert.containsOnce(root, ".o_content .o_toy_b_view .o_content .o_toy_c_view .o_content"); // Full chain of contents
|
||||
assert.containsN(root, ".o_control_panel", 2); // Component C has hidden its control panel
|
||||
assert.containsN(root, ".o_content.o_component_with_search_panel", 3);
|
||||
assert.containsOnce(root, ".o_search_panel"); // Standard search panel
|
||||
assert.containsN(root, ".o_toy_search_panel", 2); // Custom search panels
|
||||
assert.containsOnce(root, ".toy_a_search");
|
||||
assert.containsOnce(root, ".toy_b_breadcrumbs");
|
||||
assert.containsOnce(root, ".toy_c_content");
|
||||
});
|
||||
|
||||
QUnit.test("Custom control panel", async (assert) => {
|
||||
assert.expect(3);
|
||||
|
||||
class ToyComponent extends Component {}
|
||||
ToyComponent.template = xml`
|
||||
<Layout>
|
||||
<div class="o_toy_content" />
|
||||
</Layout>`;
|
||||
ToyComponent.components = { Layout };
|
||||
|
||||
class ControlPanel extends Component {}
|
||||
ControlPanel.template = xml`<div class="o_toy_search_panel" />`;
|
||||
|
||||
const root = await makeWithSearch({
|
||||
serverData,
|
||||
Component: ToyComponent,
|
||||
resModel: "foo",
|
||||
searchViewId: false,
|
||||
config: { ControlPanel },
|
||||
});
|
||||
|
||||
assert.containsOnce(root, ".o_toy_content");
|
||||
assert.containsOnce(root, ".o_toy_search_panel");
|
||||
assert.containsNone(root, ".o_control_panel");
|
||||
});
|
||||
|
||||
QUnit.test("Custom search panel", async (assert) => {
|
||||
assert.expect(3);
|
||||
|
||||
class ToyComponent extends Component {}
|
||||
ToyComponent.template = xml`
|
||||
<Layout>
|
||||
<div class="o_toy_content" />
|
||||
</Layout>`;
|
||||
ToyComponent.components = { Layout };
|
||||
|
||||
class SearchPanel extends Component {}
|
||||
SearchPanel.template = xml`<div class="o_toy_search_panel" />`;
|
||||
|
||||
const root = await makeWithSearch({
|
||||
serverData,
|
||||
Component: ToyComponent,
|
||||
resModel: "foo",
|
||||
searchViewId: false,
|
||||
config: { SearchPanel },
|
||||
});
|
||||
|
||||
assert.containsOnce(root, ".o_toy_content");
|
||||
assert.containsOnce(root, ".o_toy_search_panel");
|
||||
assert.containsNone(root, ".o_search_panel");
|
||||
});
|
||||
|
||||
QUnit.test("Custom banner: no bannerRoute in env", async (assert) => {
|
||||
assert.expect(2);
|
||||
|
||||
class ToyComponent extends Component {}
|
||||
ToyComponent.template = xml`
|
||||
<Layout>
|
||||
<div class="o_toy_content" />
|
||||
</Layout>`;
|
||||
ToyComponent.components = { Layout };
|
||||
|
||||
class Banner extends Component {}
|
||||
Banner.template = xml`<div class="o_toy_banner" />`;
|
||||
|
||||
const root = await makeWithSearch({
|
||||
serverData,
|
||||
Component: ToyComponent,
|
||||
resModel: "foo",
|
||||
searchViewId: false,
|
||||
config: { Banner },
|
||||
});
|
||||
|
||||
assert.containsOnce(root, ".o_toy_content");
|
||||
assert.containsNone(root, ".o_toy_banner");
|
||||
});
|
||||
|
||||
QUnit.test("Custom banner: with bannerRoute in env", async (assert) => {
|
||||
assert.expect(2);
|
||||
|
||||
class ToyComponent extends Component {}
|
||||
ToyComponent.template = xml`
|
||||
<Layout>
|
||||
<div class="o_toy_content" />
|
||||
</Layout>`;
|
||||
ToyComponent.components = { Layout };
|
||||
|
||||
class Banner extends Component {}
|
||||
Banner.template = xml`<div class="o_toy_banner" />`;
|
||||
|
||||
const root = await makeWithSearch({
|
||||
serverData,
|
||||
Component: ToyComponent,
|
||||
resModel: "foo",
|
||||
searchViewId: false,
|
||||
config: { Banner, bannerRoute: "toy/banner/route" },
|
||||
});
|
||||
|
||||
assert.containsOnce(root, ".o_toy_content");
|
||||
assert.containsOnce(root, ".o_toy_banner");
|
||||
});
|
||||
});
|
||||
@@ -6,19 +6,21 @@ import { session } from "@web/session";
|
||||
import { makeFakeLocalizationService, makeFakeUserService } from "../helpers/mock_services";
|
||||
import {
|
||||
click,
|
||||
nextTick,
|
||||
legacyExtraNextTick,
|
||||
makeDeferred,
|
||||
mockDownload,
|
||||
nextTick,
|
||||
patchDate,
|
||||
patchWithCleanup,
|
||||
triggerEvent,
|
||||
triggerEvents,
|
||||
} from "../helpers/utils";
|
||||
import { makeView } from "./helpers";
|
||||
import {
|
||||
applyGroup,
|
||||
editFavoriteName,
|
||||
removeFacet,
|
||||
saveFavorite,
|
||||
selectGroup,
|
||||
setupControlPanelFavoriteMenuRegistry,
|
||||
setupControlPanelServiceRegistry,
|
||||
toggleAddCustomGroup,
|
||||
@@ -26,15 +28,13 @@ import {
|
||||
toggleFavoriteMenu,
|
||||
toggleFilterMenu,
|
||||
toggleGroupByMenu,
|
||||
toggleMenu,
|
||||
toggleMenuItem,
|
||||
toggleMenuItemOption,
|
||||
toggleSaveFavorite,
|
||||
saveFavorite,
|
||||
selectGroup,
|
||||
removeFacet,
|
||||
toggleMenu,
|
||||
} from "../search/helpers";
|
||||
import { createWebClient, doAction } from "../webclient/helpers";
|
||||
import { makeView } from "./helpers";
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
@@ -438,12 +438,14 @@ QUnit.module("Views", (hooks) => {
|
||||
<field name="foo" type="measure"/>
|
||||
</pivot>`,
|
||||
context: { someKey: true, search_default_test: 3 },
|
||||
views: [
|
||||
[2, "form"],
|
||||
[5, "kanban"],
|
||||
[false, "list"],
|
||||
[false, "pivot"],
|
||||
],
|
||||
config: {
|
||||
views: [
|
||||
[2, "form"],
|
||||
[5, "kanban"],
|
||||
[false, "list"],
|
||||
[false, "pivot"],
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
assert.hasClass(pivot.el.querySelector("table"), "o_enable_linking");
|
||||
@@ -4689,7 +4691,9 @@ QUnit.module("Views", (hooks) => {
|
||||
serverData,
|
||||
context: { search_default_small_than_0: true },
|
||||
noContentHelp: '<p class="abc">click to add a foo</p>',
|
||||
views: [[false, "search"]],
|
||||
config: {
|
||||
views: [[false, "search"]],
|
||||
},
|
||||
});
|
||||
|
||||
assert.containsOnce(pivot, ".o_view_nocontent .abc");
|
||||
@@ -4720,7 +4724,9 @@ QUnit.module("Views", (hooks) => {
|
||||
serverData,
|
||||
context: { search_default_small_than_0: true },
|
||||
noContentHelp: '<p class="abc">click to add a foo</p>',
|
||||
views: [[false, "search"]],
|
||||
config: {
|
||||
views: [[false, "search"]],
|
||||
},
|
||||
});
|
||||
|
||||
assert.hasClass(pivot.el, "o_view_sample_data");
|
||||
@@ -4753,7 +4759,9 @@ QUnit.module("Views", (hooks) => {
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
noContentHelp: '<p class="abc">click to add a foo</p>',
|
||||
views: [[false, "search"]],
|
||||
config: {
|
||||
views: [[false, "search"]],
|
||||
},
|
||||
});
|
||||
|
||||
assert.doesNotHaveClass(pivot.el, "o_view_sample_data");
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { getFixture, nextTick, patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
|
||||
import { ormService } from "@web/core/orm_service";
|
||||
import { registerCleanup } from "@web/../tests/helpers/cleanup";
|
||||
import {
|
||||
click,
|
||||
getFixture,
|
||||
makeDeferred,
|
||||
nextTick,
|
||||
patchWithCleanup,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import { setupControlPanelServiceRegistry } from "@web/../tests/search/helpers";
|
||||
import { makeView } from "@web/../tests/views/helpers";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { View } from "@web/views/view";
|
||||
import { OnboardingBanner } from "@web/views/onboarding_banner";
|
||||
import { viewService } from "@web/views/view_service";
|
||||
import { click, makeDeferred } from "../helpers/utils";
|
||||
import { View } from "@web/views/view";
|
||||
import { actionService } from "@web/webclient/actions/action_service";
|
||||
|
||||
const { Component, mount, hooks, tags } = owl;
|
||||
@@ -18,28 +22,6 @@ const { xml } = tags;
|
||||
const serviceRegistry = registry.category("services");
|
||||
const viewRegistry = registry.category("views");
|
||||
|
||||
async function makeView(params) {
|
||||
const serverData = params.serverData;
|
||||
const mockRPC = params.mockRPC;
|
||||
const env = await makeTestEnv({ serverData, mockRPC });
|
||||
|
||||
// we don't want "fields" to be added here !!!
|
||||
const props = Object.assign({}, params);
|
||||
delete props.serverData;
|
||||
delete props.mockRPC;
|
||||
|
||||
const target = getFixture();
|
||||
|
||||
const view = await mount(View, { env, props, target });
|
||||
|
||||
registerCleanup(() => view.destroy());
|
||||
|
||||
const withSearch = Object.values(view.__owl__.children)[0];
|
||||
const concreteView = Object.values(withSearch.__owl__.children)[0];
|
||||
|
||||
return concreteView;
|
||||
}
|
||||
|
||||
let serverData;
|
||||
|
||||
QUnit.module("Views", (hooks) => {
|
||||
@@ -55,6 +37,7 @@ QUnit.module("Views", (hooks) => {
|
||||
selection: [
|
||||
["omnivorous", "Omnivorous"],
|
||||
["herbivorous", "Herbivorous"],
|
||||
|
||||
["carnivorous", "Carnivorous"],
|
||||
],
|
||||
store: true,
|
||||
@@ -109,8 +92,7 @@ QUnit.module("Views", (hooks) => {
|
||||
viewRegistry.add("toy", ToyView);
|
||||
viewRegistry.add("toy_imp", ToyViewImp);
|
||||
|
||||
serviceRegistry.add("orm", ormService);
|
||||
serviceRegistry.add("view", viewService);
|
||||
setupControlPanelServiceRegistry();
|
||||
|
||||
const fakeActionService = {
|
||||
name: "action",
|
||||
@@ -140,7 +122,7 @@ QUnit.module("Views", (hooks) => {
|
||||
assert.strictEqual(arch, serverData.views["animal,false,toy"]);
|
||||
assert.deepEqual(fields, serverData.models.animal.fields);
|
||||
assert.strictEqual(info.actionMenus, undefined);
|
||||
assert.strictEqual(info.viewId, false);
|
||||
assert.strictEqual(this.env.config.viewId, false);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -174,7 +156,7 @@ QUnit.module("Views", (hooks) => {
|
||||
assert.strictEqual(arch, serverData.views["animal,1,toy"]);
|
||||
assert.deepEqual(fields, serverData.models.animal.fields);
|
||||
assert.strictEqual(info.actionMenus, undefined);
|
||||
assert.strictEqual(info.viewId, 1);
|
||||
assert.strictEqual(this.env.config.viewId, 1);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -207,13 +189,14 @@ QUnit.module("Views", (hooks) => {
|
||||
assert.strictEqual(arch, serverData.views["animal,1,toy"]);
|
||||
assert.deepEqual(fields, serverData.models.animal.fields);
|
||||
assert.strictEqual(info.actionMenus, undefined);
|
||||
assert.strictEqual(info.viewId, 1);
|
||||
assert.strictEqual(this.env.config.viewId, 1);
|
||||
},
|
||||
});
|
||||
|
||||
const view = await makeView({
|
||||
serverData,
|
||||
mockRPC: (_, args) => {
|
||||
console.log(_);
|
||||
assert.deepEqual(args.kwargs.views, [[1, "toy"]]);
|
||||
assert.deepEqual(args.kwargs.options, {
|
||||
action_id: false,
|
||||
@@ -223,7 +206,9 @@ QUnit.module("Views", (hooks) => {
|
||||
},
|
||||
resModel: "animal",
|
||||
type: "toy",
|
||||
views: [[1, "toy"]],
|
||||
config: {
|
||||
views: [[1, "toy"]],
|
||||
},
|
||||
});
|
||||
assert.hasClass(view.el, "o_toy_view");
|
||||
assert.strictEqual(view.el.innerHTML, serverData.views["animal,1,toy"]);
|
||||
@@ -242,7 +227,7 @@ QUnit.module("Views", (hooks) => {
|
||||
assert.strictEqual(arch, serverData.views["animal,false,toy"]);
|
||||
assert.deepEqual(fields, serverData.models.animal.fields);
|
||||
assert.strictEqual(info.actionMenus, undefined);
|
||||
assert.strictEqual(info.viewId, false);
|
||||
assert.strictEqual(this.env.config.viewId, false);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -261,7 +246,9 @@ QUnit.module("Views", (hooks) => {
|
||||
},
|
||||
resModel: "animal",
|
||||
type: "toy",
|
||||
views: [[false, "other"]],
|
||||
config: {
|
||||
views: [[false, "other"]],
|
||||
},
|
||||
});
|
||||
assert.hasClass(view.el, "o_toy_view");
|
||||
assert.strictEqual(view.el.innerHTML, serverData.views["animal,false,toy"]);
|
||||
@@ -279,7 +266,7 @@ QUnit.module("Views", (hooks) => {
|
||||
assert.strictEqual(arch, serverData.views["animal,1,toy"]);
|
||||
assert.deepEqual(fields, serverData.models.animal.fields);
|
||||
assert.strictEqual(info.actionMenus, undefined);
|
||||
assert.strictEqual(info.viewId, 1);
|
||||
assert.strictEqual(this.env.config.viewId, 1);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -299,10 +286,12 @@ QUnit.module("Views", (hooks) => {
|
||||
resModel: "animal",
|
||||
type: "toy",
|
||||
viewId: 1,
|
||||
views: [
|
||||
[3, "toy"],
|
||||
[false, "other"],
|
||||
],
|
||||
config: {
|
||||
views: [
|
||||
[3, "toy"],
|
||||
[false, "other"],
|
||||
],
|
||||
},
|
||||
});
|
||||
assert.hasClass(view.el, "o_toy_view");
|
||||
assert.strictEqual(view.el.innerHTML, serverData.views["animal,1,toy"]);
|
||||
@@ -319,7 +308,7 @@ QUnit.module("Views", (hooks) => {
|
||||
assert.strictEqual(arch, `<toy>Specific arch content</toy>`);
|
||||
assert.deepEqual(fields, {});
|
||||
assert.strictEqual(info.actionMenus, undefined);
|
||||
assert.strictEqual(info.viewId, undefined);
|
||||
assert.strictEqual(this.env.config.viewId, undefined);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -348,7 +337,7 @@ QUnit.module("Views", (hooks) => {
|
||||
assert.strictEqual(arch, serverData.views["animal,false,toy"]);
|
||||
assert.deepEqual(fields, serverData.models.animal.fields);
|
||||
assert.deepEqual(info.actionMenus, {});
|
||||
assert.strictEqual(info.viewId, false);
|
||||
assert.strictEqual(this.env.config.viewId, false);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -384,7 +373,7 @@ QUnit.module("Views", (hooks) => {
|
||||
assert.strictEqual(arch, `<toy>Specific arch content</toy>`);
|
||||
assert.deepEqual(fields, {});
|
||||
assert.deepEqual(info.actionMenus, {});
|
||||
assert.strictEqual(info.viewId, false);
|
||||
assert.strictEqual(this.env.config.viewId, false);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -423,7 +412,7 @@ QUnit.module("Views", (hooks) => {
|
||||
assert.strictEqual(arch, `<toy>Specific arch content</toy>`);
|
||||
assert.deepEqual(fields, {});
|
||||
assert.deepEqual(info.actionMenus, {});
|
||||
assert.strictEqual(info.viewId, undefined);
|
||||
assert.strictEqual(this.env.config.viewId, undefined);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -714,7 +703,9 @@ QUnit.module("Views", (hooks) => {
|
||||
mockRPC,
|
||||
resModel: "animal",
|
||||
type: "toy",
|
||||
views: [[1, "toy"]],
|
||||
config: {
|
||||
views: [[1, "toy"]],
|
||||
},
|
||||
});
|
||||
|
||||
assert.containsOnce(toy, "a");
|
||||
@@ -757,7 +748,9 @@ QUnit.module("Views", (hooks) => {
|
||||
serverData,
|
||||
resModel: "animal",
|
||||
type: "toy",
|
||||
views: [[1, "toy"]],
|
||||
config: {
|
||||
views: [[1, "toy"]],
|
||||
},
|
||||
});
|
||||
|
||||
assert.containsOnce(toy, "a");
|
||||
@@ -807,7 +800,9 @@ QUnit.module("Views", (hooks) => {
|
||||
serverData,
|
||||
resModel: "animal",
|
||||
type: "toy",
|
||||
views: [[1, "toy"]],
|
||||
config: {
|
||||
views: [[1, "toy"]],
|
||||
},
|
||||
});
|
||||
|
||||
assert.containsOnce(toy, "a");
|
||||
@@ -818,7 +813,7 @@ QUnit.module("Views", (hooks) => {
|
||||
assert.expect(3);
|
||||
serverData.views["animal,1,toy"] = `
|
||||
<toy banner_route="/mybody/isacage">
|
||||
<Banner t-if="props.bannerRoute" bannerRoute="props.bannerRoute"/>
|
||||
<Banner t-if="env.config.bannerRoute" />
|
||||
</toy>`;
|
||||
|
||||
const mockRPC = (route) => {
|
||||
@@ -833,7 +828,9 @@ QUnit.module("Views", (hooks) => {
|
||||
mockRPC,
|
||||
resModel: "animal",
|
||||
type: "toy",
|
||||
views: [[1, "toy"]],
|
||||
config: {
|
||||
views: [[1, "toy"]],
|
||||
},
|
||||
});
|
||||
|
||||
assert.verifySteps(["/mybody/isacage"]);
|
||||
@@ -844,7 +841,7 @@ QUnit.module("Views", (hooks) => {
|
||||
assert.expect(7);
|
||||
serverData.views["animal,1,toy"] = `
|
||||
<toy banner_route="/mybody/isacage">
|
||||
<Banner t-if="props.bannerRoute" bannerRoute="props.bannerRoute"/>
|
||||
<Banner t-if="env.config.bannerRoute" />
|
||||
</toy>`;
|
||||
|
||||
const bannerArch = `
|
||||
@@ -893,7 +890,9 @@ QUnit.module("Views", (hooks) => {
|
||||
mockRPC,
|
||||
resModel: "animal",
|
||||
type: "toy",
|
||||
views: [[1, "toy"]],
|
||||
config: {
|
||||
views: [[1, "toy"]],
|
||||
},
|
||||
});
|
||||
|
||||
assert.verifySteps(["/mybody/isacage", "js loaded", "css loaded"]);
|
||||
@@ -910,7 +909,7 @@ QUnit.module("Views", (hooks) => {
|
||||
|
||||
serverData.views["animal,1,toy"] = `
|
||||
<toy banner_route="/mybody/isacage">
|
||||
<Banner t-if="props.bannerRoute" bannerRoute="props.bannerRoute"/>
|
||||
<Banner t-if="env.config.bannerRoute" />
|
||||
</toy>`;
|
||||
|
||||
const banners = [
|
||||
@@ -938,7 +937,9 @@ QUnit.module("Views", (hooks) => {
|
||||
mockRPC,
|
||||
resModel: "animal",
|
||||
type: "toy",
|
||||
views: [[1, "toy"]],
|
||||
config: {
|
||||
views: [[1, "toy"]],
|
||||
},
|
||||
});
|
||||
|
||||
assert.verifySteps(["/mybody/isacage"]);
|
||||
@@ -954,7 +955,7 @@ QUnit.module("Views", (hooks) => {
|
||||
assert.expect(5);
|
||||
serverData.views["animal,1,toy"] = `
|
||||
<toy banner_route="/mybody/isacage">
|
||||
<Banner t-if="props.bannerRoute" bannerRoute="props.bannerRoute"/>
|
||||
<Banner t-if="env.config.bannerRoute" />
|
||||
</toy>`;
|
||||
|
||||
const bannerArch = `
|
||||
@@ -974,7 +975,9 @@ QUnit.module("Views", (hooks) => {
|
||||
mockRPC,
|
||||
resModel: "animal",
|
||||
type: "toy",
|
||||
views: [[1, "toy"]],
|
||||
config: {
|
||||
views: [[1, "toy"]],
|
||||
},
|
||||
});
|
||||
|
||||
assert.verifySteps(["/mybody/isacage"]);
|
||||
@@ -1010,7 +1013,7 @@ QUnit.module("Views", (hooks) => {
|
||||
|
||||
serverData.views["animal,1,toy"] = `
|
||||
<toy banner_route="/banner_route">
|
||||
<Banner t-if="props.bannerRoute" bannerRoute="props.bannerRoute"/>
|
||||
<Banner t-if="env.config.bannerRoute" />
|
||||
<a type="action" data-method="setTheControl" data-model="animal" />
|
||||
</toy>`;
|
||||
|
||||
@@ -1040,7 +1043,9 @@ QUnit.module("Views", (hooks) => {
|
||||
serverData,
|
||||
resModel: "animal",
|
||||
type: "toy",
|
||||
views: [[1, "toy"]],
|
||||
config: {
|
||||
views: [[1, "toy"]],
|
||||
},
|
||||
});
|
||||
|
||||
await click(toy.el.querySelector("a[data-method='setTheControl']"));
|
||||
@@ -1054,7 +1059,7 @@ QUnit.module("Views", (hooks) => {
|
||||
|
||||
serverData.views["animal,1,toy"] = `
|
||||
<toy banner_route="/mybody/isacage">
|
||||
<Banner t-if="props.bannerRoute" bannerRoute="props.bannerRoute"/>
|
||||
<Banner t-if="env.config.bannerRoute" />
|
||||
</toy>`;
|
||||
|
||||
const bannerArch = `
|
||||
@@ -1104,7 +1109,9 @@ QUnit.module("Views", (hooks) => {
|
||||
mockRPC,
|
||||
resModel: "animal",
|
||||
type: "toy",
|
||||
views: [[1, "toy"]],
|
||||
config: {
|
||||
views: [[1, "toy"]],
|
||||
},
|
||||
});
|
||||
|
||||
const prom = new Promise((resolve) => {
|
||||
@@ -1252,7 +1259,7 @@ QUnit.module("Views", (hooks) => {
|
||||
QUnit.test("'resModel' must be passed as prop", async function (assert) {
|
||||
assert.expect(2);
|
||||
try {
|
||||
await makeView({ serverData });
|
||||
await makeView({ serverData }, { noFields: true });
|
||||
} catch (error) {
|
||||
assert.step(error.message);
|
||||
}
|
||||
@@ -1272,7 +1279,10 @@ QUnit.module("Views", (hooks) => {
|
||||
QUnit.test("'arch' cannot be passed as prop alone", async function (assert) {
|
||||
assert.expect(2);
|
||||
try {
|
||||
await makeView({ serverData, resModel: "animal", type: "toy", arch: "<toy/>" });
|
||||
await makeView(
|
||||
{ serverData, resModel: "animal", type: "toy", arch: "<toy/>" },
|
||||
{ noFields: true }
|
||||
);
|
||||
} catch (error) {
|
||||
assert.step(error.message);
|
||||
}
|
||||
|
||||
@@ -313,9 +313,9 @@ QUnit.module("ActionManager", (hooks) => {
|
||||
class ClientAction extends Component {
|
||||
setup() {
|
||||
this.breadcrumbTitle = "myOwlAction";
|
||||
const breadCrumbs = this.props.breadcrumbs;
|
||||
assert.strictEqual(breadCrumbs.length, 1);
|
||||
assert.strictEqual(breadCrumbs[0].name, "Favorite Ponies");
|
||||
const { breadcrumbs } = this.env.config;
|
||||
assert.strictEqual(breadcrumbs.length, 1);
|
||||
assert.strictEqual(breadcrumbs[0].name, "Favorite Ponies");
|
||||
}
|
||||
mounted() {
|
||||
this.trigger("controller-title-updated", this.breadcrumbTitle);
|
||||
@@ -422,10 +422,12 @@ QUnit.module("ActionManager", (hooks) => {
|
||||
title: "title",
|
||||
message: "message %s <R&D>",
|
||||
sticky: true,
|
||||
links: [{
|
||||
label: "test <R&D>",
|
||||
url: '#action={action.id}&id={order.id}&model=purchase.order',
|
||||
}],
|
||||
links: [
|
||||
{
|
||||
label: "test <R&D>",
|
||||
url: "#action={action.id}&id={order.id}&model=purchase.order",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const notificationSelector = ".o_notification_manager .o_notification";
|
||||
@@ -460,10 +462,12 @@ QUnit.module("ActionManager", (hooks) => {
|
||||
params: {
|
||||
message: "message %s <R&D>",
|
||||
sticky: true,
|
||||
links: [{
|
||||
label: "test <R&D>",
|
||||
url: '#action={action.id}&id={order.id}&model=purchase.order',
|
||||
}],
|
||||
links: [
|
||||
{
|
||||
label: "test <R&D>",
|
||||
url: "#action={action.id}&id={order.id}&model=purchase.order",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
assert.containsOnce(
|
||||
@@ -473,7 +477,8 @@ QUnit.module("ActionManager", (hooks) => {
|
||||
);
|
||||
notificationElement = document.body.querySelector(notificationSelector);
|
||||
assert.containsNone(
|
||||
notificationElement, ".o_notification_title",
|
||||
notificationElement,
|
||||
".o_notification_title",
|
||||
"the notification should not have title"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -652,7 +652,7 @@ QUnit.module("ActionManager", (hooks) => {
|
||||
ToyView.searchMenuTypes = ["filter"];
|
||||
ToyView.template = owl.tags.xml`
|
||||
<div class="o_toy_view">
|
||||
<ControlPanel t-props="props.info"/>
|
||||
<ControlPanel />
|
||||
</div>
|
||||
`;
|
||||
ToyView.type = "toy";
|
||||
|
||||
Reference in New Issue
Block a user