[REF] web, *: improve the way views are defined, reintroduce controllers

Earlier in Odoo, JS views were defined by defining 4 elements: View,
Controller, Model, Renderer.  This was complex in some way, because we
wanted to inherit behaviour as well, so it was necessary to think along
multiple dimensions to understand how the code was running.

Then, with Owl, we rewrote some views, and simplified them: views were
now just a Component. Most of the common behaviour now came from the
generic View component that instantiated the concrete view with the
proper informations.  In practice, views were still split in views
(which was the equivalent of the Controller of earlier views), Model and
Renderer

Now, this commit reintroduce the Controller, and change the way views
are defined: by an object with multiple metadata, and an (optional)
props function to compute the actual props used by the view.

As a result, views are now much easier to extend/modify.

closes odoo/odoo#89889

Related: odoo/enterprise#26728
Signed-off-by: Géry Debongnie <ged@odoo.com>
This commit is contained in:
Géry Debongnie
2022-05-02 22:38:02 +02:00
parent b9218b5800
commit b828cfc72c
24 changed files with 529 additions and 479 deletions
@@ -3,10 +3,10 @@ import { ForecastKanbanController } from './forecast_controllers';
import { ForecastKanbanModel } from './forecast_models';
import { ForecastKanbanRenderer } from './forecast_renderers';
import { ForecastSearchModel } from "./forecast_search_model";
import { GraphView } from "@web/views/graph/graph_view";
import { graphView } from "@web/views/graph/graph_view";
import KanbanView from 'web.KanbanView';
import ListView from 'web.ListView';
import { PivotView } from "@web/views/pivot/pivot_view";
import { pivotView } from "@web/views/pivot/pivot_view";
import { registry } from "@web/core/registry";
import viewRegistry from 'web.view_registry';
@@ -16,10 +16,12 @@ import viewRegistry from 'web.view_registry';
* - context key `forecast_field` on a date/datetime field
* - special filter "Forecast" (which must set the `forecast_filter:1` context key)
*/
class ForecastGraphView extends GraphView {}
ForecastGraphView.SearchModel = ForecastSearchModel;
export const forecastGraphView = {
...graphView,
SearchModel: ForecastSearchModel,
};
registry.category("views").add("forecast_graph", ForecastGraphView);
registry.category("views").add("forecast_graph", forecastGraphView);
/**
* Kanban view to be used for a Forecast
@@ -71,14 +73,14 @@ viewRegistry.add('forecast_list', ForecastListView);
* - context key `forecast_field` on a date/datetime field
* - special filter "Forecast" (which must set the `forecast_filter:1` context key)
*/
class ForecastPivotView extends PivotView {}
ForecastPivotView.SearchModel = ForecastSearchModel;
export const forecastPivotView = {
...pivotView,
SearchModel: ForecastSearchModel,
};
registry.category("views").add("forecast_pivot", ForecastPivotView);
registry.category("views").add("forecast_pivot", forecastPivotView);
export {
ForecastGraphView,
ForecastKanbanView,
ForecastListView,
ForecastPivotView,
};
@@ -1,8 +1,8 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { GraphView } from "@web/views/graph/graph_view";
import { PivotView } from "@web/views/pivot/pivot_view";
import { graphView } from "@web/views/graph/graph_view";
import { pivotView } from "@web/views/pivot/pivot_view";
const viewRegistry = registry.category("views");
@@ -22,7 +22,7 @@ function openView(component, domain, views, context) {
});
}
export class AttendanceReportGraphView extends GraphView {
export class AttendanceReportGraphController extends graphView.Controller {
/**
* @override
*/
@@ -31,9 +31,12 @@ export class AttendanceReportGraphView extends GraphView {
}
}
viewRegistry.add("attendance_report_graph", AttendanceReportGraphView);
viewRegistry.add("attendance_report_graph", {
...graphView,
Controller: AttendanceReportGraphController
});
export class AttendanceReportPivotView extends PivotView {
export class AttendanceReportPivotController extends pivotView.Controller {
/**
* @override
*/
@@ -42,4 +45,7 @@ export class AttendanceReportPivotView extends PivotView {
}
}
viewRegistry.add("attendance_report_pivot", AttendanceReportPivotView);
viewRegistry.add("attendance_report_pivot", {
...pivotView,
Controller: AttendanceReportPivotController
});
@@ -1,12 +1,14 @@
/** @odoo-module **/
import { ProjectGraphView } from "@project/js/project_graph_view";
import { projectGraphView } from "@project/js/project_graph_view";
import { hrTimesheetGraphModel } from "./timesheet_graph_model";
import { registry } from "@web/core/registry";
const viewRegistry = registry.category("views");
export class hrTimesheetGraphView extends ProjectGraphView {}
hrTimesheetGraphView.Model = hrTimesheetGraphModel;
export const hrTimesheetGraphView = {
...projectGraphView,
Model: hrTimesheetGraphModel,
};
viewRegistry.add("hr_timesheet_graphview", hrTimesheetGraphView);
@@ -1,12 +1,10 @@
/** @odoo-module **/
import { BurndownChartPivotModel } from "./burndown_chart_pivot_model";
import { PivotView } from "@web/views/pivot/pivot_view";
import { pivotView } from "@web/views/pivot/pivot_view";
import { registry } from "@web/core/registry";
const viewRegistry = registry.category("views");
class BurndownChartPivotView extends PivotView {}
BurndownChartPivotView.Model = BurndownChartPivotModel;
viewRegistry.add("burndown_chart_pivot", BurndownChartPivotView);
const burndownChartPivotView = {...pivotView, Model: BurndownChartPivotModel};
viewRegistry.add("burndown_chart_pivot", burndownChartPivotView);
@@ -2,14 +2,16 @@
import { BurndownChartModel } from "./burndown_chart_model";
import { BurndownChartRenderer } from "./burndown_chart_renderer";
import { GraphView } from "@web/views/graph/graph_view";
import { graphView } from "@web/views/graph/graph_view";
import { registry } from "@web/core/registry";
const viewRegistry = registry.category("views");
class BurndownChartView extends GraphView {}
BurndownChartView.components = { ...GraphView.components, Renderer: BurndownChartRenderer };
BurndownChartView.buttonTemplate = "project.BurndownChartView.Buttons";
BurndownChartView.Model = BurndownChartModel;
const burndownChartGraphView = {
...graphView,
Renderer: BurndownChartRenderer,
buttonTemplate: "project.BurndownChartView.Buttons",
Model: BurndownChartModel,
};
viewRegistry.add("burndown_chart", BurndownChartView);
viewRegistry.add("burndown_chart", burndownChartGraphView);
@@ -2,11 +2,10 @@
import { ProjectControlPanel } from "@project/project_control_panel/project_control_panel";
import { registry } from "@web/core/registry";
import { GraphView } from "@web/views/graph/graph_view";
import { graphView } from "@web/views/graph/graph_view";
const viewRegistry = registry.category("views");
export class ProjectGraphView extends GraphView {}
ProjectGraphView.ControlPanel = ProjectControlPanel;
export const projectGraphView = {...graphView, ControlPanel: ProjectControlPanel};
viewRegistry.add("project_graph", ProjectGraphView);
viewRegistry.add("project_graph", projectGraphView);
@@ -2,11 +2,8 @@
import { ProjectControlPanel } from "@project/project_control_panel/project_control_panel";
import { registry } from "@web/core/registry";
import { PivotView } from "@web/views/pivot/pivot_view";
import { pivotView } from "@web/views/pivot/pivot_view";
const viewRegistry = registry.category("views");
const projectPivotView = {...pivotView, ControlPanel: ProjectControlPanel};
class ProjectPivotView extends PivotView {}
ProjectPivotView.ControlPanel = ProjectControlPanel;
viewRegistry.add("project_pivot", ProjectPivotView);
registry.category("views").add("project_pivot", projectPivotView);
@@ -3,7 +3,7 @@
import { _lt } from "@web/core/l10n/translation";
import { registry } from "@web/core/registry";
import { GraphArchParser } from "@web/views/graph/graph_arch_parser";
import { GraphView } from "@web/views/graph/graph_view";
import { graphView } from "@web/views/graph/graph_view";
const viewRegistry = registry.category("views");
@@ -27,8 +27,9 @@ class ProjectRatingArchParser extends GraphArchParser {
}
// Would it be not better achiedved by using a proper arch directly?
const projectRatingGraphView = {
...graphView,
ArchParser: ProjectRatingArchParser,
};
class ProjectRatingGraphView extends GraphView {}
ProjectRatingGraphView.ArchParser = ProjectRatingArchParser;
viewRegistry.add("project_rating_graph", ProjectRatingGraphView);
viewRegistry.add("project_rating_graph", projectRatingGraphView);
@@ -3,7 +3,7 @@
import { _lt } from "@web/core/l10n/translation";
import { registry } from "@web/core/registry";
import { PivotArchParser } from "@web/views/pivot/pivot_arch_parser";
import { PivotView } from "@web/views/pivot/pivot_view";
import { pivotView } from "@web/views/pivot/pivot_view";
const viewRegistry = registry.category("views");
@@ -28,7 +28,9 @@ class ProjectRatingArchParser extends PivotArchParser {
// Would it be not better achiedved by using a proper arch directly?
class ProjectRatingPivotView extends PivotView {}
ProjectRatingPivotView.ArchParser = ProjectRatingArchParser;
const projectRatingPivotView = {
...pivotView,
ArchParser: ProjectRatingArchParser,
};
viewRegistry.add("project_rating_pivot", ProjectRatingPivotView);
viewRegistry.add("project_rating_pivot", projectRatingPivotView);
@@ -17,7 +17,7 @@ export class WithSearch extends Component {
useSubEnv({ __getContext__: new CallbackRecorder() });
}
const SearchModelClass = this.Component.SearchModel || SearchModel;
const SearchModelClass = this.props.SearchModel || SearchModel;
this.searchModel = new SearchModelClass(this.env, {
user: useService("user"),
orm: useService("orm"),
@@ -77,6 +77,7 @@ WithSearch.defaultProps = {
};
WithSearch.props = {
Component: Function,
SearchModel: { type: Function, optional: true },
componentProps: { type: Object, optional: true },
resModel: String,
@@ -0,0 +1,134 @@
/** @odoo-module **/
import { Dropdown } from "@web/core/dropdown/dropdown";
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
import { useService } from "@web/core/utils/hooks";
import { Layout } from "@web/search/layout";
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 { useModel } from "../helpers/model";
const { Component } = owl;
export class GraphController extends Component {
setup() {
this.actionService = useService("action");
this.model = useModel(this.props.Model, this.props.modelParams);
useSetupView({
getLocalState: () => {
return { metaData: this.model.metaData };
},
getContext: () => this.getContext(),
});
}
/**
* @returns {Object}
*/
getContext() {
// expand context object? change keys?
const { measure, groupBy, mode } = this.model.metaData;
return {
graph_measure: measure,
graph_mode: mode,
graph_groupbys: groupBy.map((gb) => gb.spec),
};
}
/**
* Execute the action to open the view on the current model.
*
* @param {Array} domain
* @param {Array} views
* @param {Object} context
*/
openView(domain, views, context) {
this.actionService.doAction(
{
context,
domain,
name: this.model.metaData.title,
res_model: this.model.metaData.resModel,
target: "current",
type: "ir.actions.act_window",
views,
},
{
viewType: "list",
}
);
}
/**
* @param {string} domain the domain of the clicked area
*/
onGraphClicked(domain) {
const { context } = this.model.metaData;
Object.keys(context).forEach((x) => {
if (x === "group_by" || x.startsWith("search_default_")) {
delete context[x];
}
});
const views = {};
for (const [viewId, viewType] of this.env.config.views || []) {
views[viewType] = viewId;
}
function getView(viewType) {
return [views[viewType] || false, viewType];
}
const actionViews = [getView("list"), getView("form")];
this.openView(domain, actionViews, context);
}
/**
* @param {Object} param0
* @param {string} param0.measure
*/
onMeasureSelected({ measure }) {
this.model.updateMetaData({ measure });
}
/**
* @param {"bar"|"line"|"pie"} mode
*/
onModeSelected(mode) {
this.model.updateMetaData({ mode });
}
/**
* @param {"ASC"|"DESC"} order
*/
toggleOrder(order) {
const { order: currentOrder } = this.model.metaData;
const nextOrder = currentOrder === order ? null : order;
this.model.updateMetaData({ order: nextOrder });
}
toggleStacked() {
const { stacked } = this.model.metaData;
this.model.updateMetaData({ stacked: !stacked });
}
}
GraphController.template = "web.GraphView";
GraphController.components = { Dropdown, DropdownItem, GroupByMenu, Layout };
GraphController.defaultProps = {
additionalMeasures: [],
displayGroupByMenu: false,
displayScaleLabels: true,
};
GraphController.props = {
...standardViewProps,
additionalMeasures: { type: Array, elements: String, optional: true },
displayGroupByMenu: { type: Boolean, optional: true },
displayScaleLabels: { type: Boolean, optional: true },
Model: Function,
modelParams: Object,
Renderer: Function,
buttonTemplate: String,
};
@@ -49,13 +49,13 @@
<div t-att-class="props.className">
<Layout className="model.useSampleModel ? 'o_view_sample_data' : ''">
<t t-set-slot="control-panel-bottom-left">
<t t-call="{{ constructor.buttonTemplate }}"/>
<t t-call="{{ props.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>
<Renderer
<t t-component="props.Renderer"
class="model.useSampleModel ? 'o_sample_data_disabled' : ''"
model="model"
onGraphClicked="(domain) => this.onGraphClicked(domain)"
+29 -147
View File
@@ -1,176 +1,58 @@
/** @odoo-module **/
import { Dropdown } from "@web/core/dropdown/dropdown";
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
import { _lt } from "@web/core/l10n/translation";
import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks";
import { Layout } from "@web/search/layout";
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 { useModel } from "../helpers/model";
import { GraphArchParser } from "./graph_arch_parser";
import { GraphModel } from "./graph_model";
import { GraphController } from "./graph_controller";
import { GraphRenderer } from "./graph_renderer";
const viewRegistry = registry.category("views");
const { Component } = owl;
export class GraphView extends Component {
setup() {
this.actionService = useService("action");
export const graphView = {
type: "graph",
display_name: _lt("Graph"),
icon: "fa fa-area-chart",
multiRecord: true,
Controller: GraphController,
Renderer: GraphRenderer,
Model: GraphModel,
ArchParser: GraphArchParser,
searchMenuTypes: ["filter", "groupBy", "comparison", "favorite"],
buttonTemplate: "web.GraphView.Buttons",
props: (genericProps, view) => {
let modelParams;
if (this.props.state) {
modelParams = this.props.state.metaData;
if (genericProps.state) {
modelParams = genericProps.state.metaData;
} else {
const { arch, fields } = this.props;
const parser = new this.constructor.ArchParser();
const { arch, fields, resModel, additionalMeasures, displayScaleLabels } = genericProps;
const parser = new view.ArchParser();
const archInfo = parser.parse(arch, fields);
modelParams = {
additionalMeasures: this.props.additionalMeasures,
additionalMeasures: additionalMeasures,
disableLinking: Boolean(archInfo.disableLinking),
displayScaleLabels: this.props.displayScaleLabels,
displayScaleLabels: displayScaleLabels,
fieldAttrs: archInfo.fieldAttrs,
fields: this.props.fields,
fields: fields,
groupBy: archInfo.groupBy,
measure: archInfo.measure || "__count",
mode: archInfo.mode || "bar",
order: archInfo.order || null,
resModel: this.props.resModel,
resModel: resModel,
stacked: "stacked" in archInfo ? archInfo.stacked : true,
title: archInfo.title || this.env._t("Untitled"),
title: archInfo.title || _lt("Untitled"),
};
}
this.model = useModel(this.constructor.Model, modelParams);
useSetupView({
getLocalState: () => {
return { metaData: this.model.metaData };
},
getContext: () => this.getContext(),
});
}
/**
* @returns {Object}
*/
getContext() {
// expand context object? change keys?
const { measure, groupBy, mode } = this.model.metaData;
return {
graph_measure: measure,
graph_mode: mode,
graph_groupbys: groupBy.map((gb) => gb.spec),
...genericProps,
modelParams,
Model: view.Model,
Renderer: view.Renderer,
buttonTemplate: view.buttonTemplate,
};
}
/**
* Execute the action to open the view on the current model.
*
* @param {Array} domain
* @param {Array} views
* @param {Object} context
*/
openView(domain, views, context) {
this.actionService.doAction(
{
context,
domain,
name: this.model.metaData.title,
res_model: this.model.metaData.resModel,
target: "current",
type: "ir.actions.act_window",
views: views,
},
{
viewType: "list",
}
);
}
/**
* @param {string} domain the domain of the clicked area
*/
onGraphClicked(domain) {
const { context } = this.model.metaData;
Object.keys(context).forEach((x) => {
if (x === "group_by" || x.startsWith("search_default_")) {
delete context[x];
}
});
const views = {};
for (const [viewId, viewType] of this.env.config.views || []) {
views[viewType] = viewId;
}
function getView(viewType) {
return [views[viewType] || false, viewType];
}
const actionViews = [getView("list"), getView("form")];
this.openView(domain, actionViews, context);
}
/**
* @param {Object} param0
* @param {string} param0.measure
*/
onMeasureSelected({ measure }) {
this.model.updateMetaData({ measure });
}
/**
* @param {"bar"|"line"|"pie"} mode
*/
onModeSelected(mode) {
this.model.updateMetaData({ mode });
}
/**
* @param {"ASC"|"DESC"} order
*/
toggleOrder(order) {
const { order: currentOrder } = this.model.metaData;
const nextOrder = currentOrder === order ? null : order;
this.model.updateMetaData({ order: nextOrder });
}
toggleStacked() {
const { stacked } = this.model.metaData;
this.model.updateMetaData({ stacked: !stacked });
}
}
GraphView.template = "web.GraphView";
GraphView.buttonTemplate = "web.GraphView.Buttons";
GraphView.components = { Dropdown, DropdownItem, GroupByMenu, Renderer: GraphRenderer, Layout };
GraphView.defaultProps = {
additionalMeasures: [],
displayGroupByMenu: false,
displayScaleLabels: true,
},
};
GraphView.props = {
...standardViewProps,
additionalMeasures: { type: Array, elements: String, optional: true },
displayGroupByMenu: { type: Boolean, optional: true },
displayScaleLabels: { type: Boolean, optional: true },
};
GraphView.type = "graph";
GraphView.display_name = _lt("Graph");
GraphView.icon = "fa fa-area-chart";
GraphView.multiRecord = true;
GraphView.Model = GraphModel;
GraphView.ArchParser = GraphArchParser;
GraphView.searchMenuTypes = ["filter", "groupBy", "comparison", "favorite"];
viewRegistry.add("graph", GraphView);
viewRegistry.add("graph", graphView);
@@ -0,0 +1,145 @@
/** @odoo-module **/
import { Dropdown } from "@web/core/dropdown/dropdown";
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
import { download } from "@web/core/network/download";
import { useService } from "@web/core/utils/hooks";
import { Layout } from "@web/search/layout";
import { useModel } from "@web/views/helpers/model";
import { standardViewProps } from "@web/views/helpers/standard_view_props";
import { useSetupView } from "@web/views/helpers/view_hook";
const { Component } = owl;
export class PivotController extends Component {
setup() {
this.actionService = useService("action");
this.model = useModel(this.props.Model, this.props.modelParams);
useSetupView({
getLocalState: () => {
const { data, metaData } = this.model;
return { data, metaData };
},
getContext: () => this.getContext(),
});
}
/**
* @returns {Object}
*/
getContext() {
return {
pivot_measures: this.model.metaData.activeMeasures,
pivot_column_groupby: this.model.metaData.fullColGroupBys,
pivot_row_groupby: this.model.metaData.fullRowGroupBys,
};
}
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Exports the current pivot table data in a xls file. For this, we have to
* serialize the current state, then call the server /web/pivot/export_xlsx.
* Force a reload before exporting to ensure to export up-to-date data.
*/
onDownloadButtonClicked() {
if (this.model.getTableWidth() > 16384) {
throw new Error(
this.env._t(
"For Excel compatibility, data cannot be exported if there are more than 16384 columns.\n\nTip: try to flip axis, filter further or reduce the number of measures."
)
);
}
const table = this.model.exportData();
download({
url: "/web/pivot/export_xlsx",
data: { data: JSON.stringify(table) },
});
}
/**
* Expands all groups
*/
onExpandButtonClicked() {
this.model.expandAll();
}
/**
* Flips axis
*/
onFlipButtonClicked() {
this.model.flip();
}
/**
* Toggles the given measure
*
* @param {Object} param0
* @param {string} param0.measure
*/
onMeasureSelected({ measure }) {
this.model.toggleMeasure(measure);
}
/**
* Execute the action to open the view on the current model.
*
* @param {Array} domain
* @param {Array} views
* @param {Object} context
*/
openView(domain, views, context) {
this.actionService.doAction({
type: "ir.actions.act_window",
name: this.model.metaData.title,
res_model: this.props.resModel,
views: views,
view_mode: "list",
target: "current",
context,
domain,
});
}
/**
* @param {CustomEvent} ev
*/
onOpenView(cell) {
if (cell.value === undefined || this.model.metaData.disableLinking) {
return;
}
const context = Object.assign({}, this.model.searchParams.context);
Object.keys(context).forEach((x) => {
if (x === "group_by" || x.startsWith("search_default_")) {
delete context[x];
}
});
// retrieve form and list view ids from the action
const { views = [] } = this.env.config;
this.views = ["list", "form"].map((viewType) => {
const view = views.find((view) => view[1] === viewType);
return [view ? view[0] : false, viewType];
});
const group = {
rowValues: cell.groupId[0],
colValues: cell.groupId[1],
originIndex: cell.originIndexes[0],
};
this.openView(this.model.getGroupDomain(group), this.views, context);
}
}
PivotController.template = "web.PivotView";
PivotController.components = { Dropdown, DropdownItem, Layout };
PivotController.props = {
...standardViewProps,
additionalMeasures: { type: Array, elements: String, optional: 1 },
Model: Function,
modelParams: Object,
Renderer: Function,
buttonTemplate: String,
};
PivotController.defaultProps = {
additionalMeasures: [],
};
@@ -20,7 +20,7 @@
<div t-att-class="props.className">
<Layout className="model.useSampleModel ? 'o_view_sample_data' : ''">
<t t-set-slot="control-panel-bottom-left">
<t t-call="{{ constructor.buttonTemplate }}"/>
<t t-call="{{ props.buttonTemplate }}"/>
</t>
<t t-set="displayNoContent" t-value="
props.info.noContentHelp !== false and (
@@ -35,7 +35,7 @@
<t t-else="" t-call="web.NoContentHelper"/>
</t>
<t t-if="model.hasData() and model.metaData.activeMeasures.length">
<Renderer model="model" onCellClicked="cell => this.onOpenView(cell)"/>
<t t-component="props.Renderer" model="model" onCellClicked="cell => this.onOpenView(cell)"/>
</t>
</Layout>
</div>
+29 -155
View File
@@ -1,35 +1,36 @@
/** @odoo-module **/
import { Dropdown } from "@web/core/dropdown/dropdown";
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
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 { Layout } from "@web/search/layout";
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 { PivotArchParser } from "@web/views/pivot/pivot_arch_parser";
import { PivotController } from "./pivot_controller";
import { PivotModel } from "@web/views/pivot/pivot_model";
import { PivotRenderer } from "@web/views/pivot/pivot_renderer";
const viewRegistry = registry.category("views");
const { Component } = owl;
export class PivotView extends Component {
setup() {
this.actionService = useService("action");
export const pivotView = {
type: "pivot",
display_name: _lt("Pivot"),
icon: "oi oi-view-pivot",
multiRecord: true,
Controller: PivotController,
Renderer: PivotRenderer,
Model: PivotModel,
ArchParser: PivotArchParser,
searchMenuTypes: ["filter", "groupBy", "comparison", "favorite"],
buttonTemplate: "web.PivotView.Buttons",
props: (genericProps, view) => {
let modelParams = {};
if (this.props.state) {
modelParams.data = this.props.state.data;
modelParams.metaData = this.props.state.metaData;
if (genericProps.state) {
modelParams.data = genericProps.state.data;
modelParams.metaData = genericProps.state.metaData;
} else {
const { arch } = this.props;
const { arch, additionalMeasures, fields, resModel } = genericProps;
// parse arch
const archInfo = new this.constructor.ArchParser().parse(arch);
const archInfo = new view.ArchParser().parse(arch);
if (!archInfo.activeMeasures.length || archInfo.displayQuantity) {
archInfo.activeMeasures.unshift("__count");
@@ -37,154 +38,27 @@ export class PivotView extends Component {
modelParams.metaData = {
activeMeasures: archInfo.activeMeasures,
additionalMeasures: this.props.additionalMeasures,
additionalMeasures: additionalMeasures,
colGroupBys: archInfo.colGroupBys,
defaultOrder: archInfo.defaultOrder,
disableLinking: Boolean(archInfo.disableLinking),
fields: this.props.fields,
fields: fields,
fieldAttrs: archInfo.fieldAttrs,
resModel: this.props.resModel,
resModel: resModel,
rowGroupBys: archInfo.rowGroupBys,
title: archInfo.title || this.env._t("Untitled"),
title: archInfo.title || _lt("Untitled"),
widgets: archInfo.widgets,
};
}
this.model = useModel(this.constructor.Model, modelParams);
useSetupView({
getLocalState: () => {
const { data, metaData } = this.model;
return { data, metaData };
},
getContext: () => this.getContext(),
});
}
/**
* @returns {Object}
*/
getContext() {
return {
pivot_measures: this.model.metaData.activeMeasures,
pivot_column_groupby: this.model.metaData.fullColGroupBys,
pivot_row_groupby: this.model.metaData.fullRowGroupBys,
...genericProps,
Model: view.Model,
modelParams,
Renderer: view.Renderer,
buttonTemplate: view.buttonTemplate,
};
}
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Exports the current pivot table data in a xls file. For this, we have to
* serialize the current state, then call the server /web/pivot/export_xlsx.
* Force a reload before exporting to ensure to export up-to-date data.
*/
onDownloadButtonClicked() {
if (this.model.getTableWidth() > 16384) {
throw new Error(
this.env._t(
"For Excel compatibility, data cannot be exported if there are more than 16384 columns.\n\nTip: try to flip axis, filter further or reduce the number of measures."
)
);
}
const table = this.model.exportData();
download({
url: "/web/pivot/export_xlsx",
data: { data: JSON.stringify(table) },
});
}
/**
* Expands all groups
*/
onExpandButtonClicked() {
this.model.expandAll();
}
/**
* Flips axis
*/
onFlipButtonClicked() {
this.model.flip();
}
/**
* Toggles the given measure
*
* @param {Object} param0
* @param {string} param0.measure
*/
onMeasureSelected({ measure }) {
this.model.toggleMeasure(measure);
}
/**
* Execute the action to open the view on the current model.
*
* @param {Array} domain
* @param {Array} views
* @param {Object} context
*/
openView(domain, views, context) {
this.actionService.doAction({
type: "ir.actions.act_window",
name: this.model.metaData.title,
res_model: this.props.resModel,
views: views,
view_mode: "list",
target: "current",
context,
domain,
});
}
/**
* @param {CustomEvent} ev
*/
onOpenView(cell) {
if (cell.value === undefined || this.model.metaData.disableLinking) {
return;
}
const context = Object.assign({}, this.model.searchParams.context);
Object.keys(context).forEach((x) => {
if (x === "group_by" || x.startsWith("search_default_")) {
delete context[x];
}
});
// retrieve form and list view ids from the action
const { views = [] } = this.env.config;
this.views = ["list", "form"].map((viewType) => {
const view = views.find((view) => view[1] === viewType);
return [view ? view[0] : false, viewType];
});
const group = {
rowValues: cell.groupId[0],
colValues: cell.groupId[1],
originIndex: cell.originIndexes[0],
};
this.openView(this.model.getGroupDomain(group), this.views, context);
}
}
PivotView.template = "web.PivotView";
PivotView.buttonTemplate = "web.PivotView.Buttons";
PivotView.components = { Dropdown, DropdownItem, Renderer: PivotRenderer, Layout };
PivotView.props = {
...standardViewProps,
additionalMeasures: { type: Array, elements: String, optional: 1 },
};
PivotView.defaultProps = {
additionalMeasures: [],
},
};
PivotView.Model = PivotModel;
PivotView.ArchParser = PivotArchParser;
PivotView.type = "pivot";
PivotView.display_name = _lt("Pivot");
PivotView.icon = "oi oi-view-pivot";
PivotView.multiRecord = true;
PivotView.searchMenuTypes = ["filter", "groupBy", "comparison", "favorite"];
viewRegistry.add("pivot", PivotView);
viewRegistry.add("pivot", pivotView);
+9 -7
View File
@@ -162,8 +162,8 @@ export class View extends Component {
async onWillStart() {
// determine view type
let ViewClass = viewRegistry.get(this.props.type);
const type = ViewClass.type;
let descr = viewRegistry.get(this.props.type);
const type = descr.type;
// determine views for which descriptions should be obtained
let { viewId, searchViewId } = this.props;
@@ -249,7 +249,7 @@ export class View extends Component {
// determine ViewClass to instantiate (if not already done)
if (subType) {
ViewClass = viewRegistry.get(subType);
descr = viewRegistry.get(subType);
}
Object.assign(this.env.config, {
@@ -257,7 +257,7 @@ export class View extends Component {
viewType: type,
viewSubType: subType,
bannerRoute,
...extractLayoutComponents(ViewClass),
...extractLayoutComponents(descr),
});
// prepare the view props
@@ -291,11 +291,13 @@ export class View extends Component {
viewProps.info.noContentHelp = noContentHelp;
}
const finalProps = descr.props ? descr.props(viewProps, descr, this.env.config) : viewProps;
// prepare the WithSearch component props
this.withSearchProps = {
...toRaw(this.props),
Component: ViewClass,
componentProps: viewProps,
Component: descr.Controller,
SearchModel: descr.SearchModel,
componentProps: finalProps,
};
if (searchViewId !== undefined) {
@@ -312,7 +314,7 @@ export class View extends Component {
if (!this.withSearchProps.searchMenuTypes) {
this.withSearchProps.searchMenuTypes =
this.props.searchMenuTypes ||
ViewClass.searchMenuTypes ||
descr.searchMenuTypes ||
this.constructor.searchMenuTypes;
}
@@ -422,15 +422,18 @@ QUnit.module("DebugMenu", (hooks) => {
// removing the toy view and using the kanban view directly
prepareRegistriesWithCleanup();
class ToyView extends Component {
class ToyController extends Component {
setup() {
useSetupView();
}
}
ToyView.template = xml`<div class="o-toy-view"/>`;
ToyView.type = "toy";
ToyView.display_name = "toy view";
registry.category("views").add("toy", ToyView);
ToyController.template = xml`<div class="o-toy-view"/>`;
registry.category("views").add("toy", {
type: "toy",
display_name: "toy view",
Controller: ToyController,
});
const mockRPC = async (route, args) => {
if (args.method === "check_access_rights") {
@@ -79,18 +79,17 @@ QUnit.module("Views", (hooks) => {
setupControlPanelServiceRegistry();
serviceRegistry.add("dialog", dialogService);
class ToyView extends LegacyComponent {}
ToyView.components = { ControlPanel };
ToyView.display_name = _lt("Toy view");
ToyView.icon = "fab fa-android";
ToyView.multiRecord = true;
ToyView.searchMenuTypes = ["filter", "groupBy", "comparison", "favorite"];
ToyView.template = xml`
<div class="o_toy_view">
<ControlPanel />
</div>
`;
ToyView.type = "toy";
class ToyController extends LegacyComponent {}
ToyController.template = xml`<div class="o_toy_view"><ControlPanel /></div>`;
ToyController.components = { ControlPanel};
viewRegistry.add("toy", {
type: "toy",
display_name: _lt("Toy view"),
multiRecord: true,
searchMenuTypes: ["filter", "groupBy", "comparison", "favorite"],
Controller: ToyController,
});
const LegacyToyView = AbstractView.extend({
display_name: _lt("Legacy toy view"),
@@ -100,7 +99,6 @@ QUnit.module("Views", (hooks) => {
searchMenuTypes: ["filter", "groupBy", "comparison", "favorite"],
});
viewRegistry.add("toy", ToyView);
legacyViewRegistry.add("legacy_toy", LegacyToyView);
});
@@ -130,7 +130,7 @@ QUnit.module("Search", (hooks) => {
QUnit.test("delete an active favorite", async function (assert) {
assert.expect(11);
class ToyView extends Component {
class ToyController extends Component {
setup() {
assert.deepEqual(this.props.domain, [["foo", "=", "qsdf"]]);
onWillUpdateProps((nextProps) => {
@@ -138,17 +138,19 @@ QUnit.module("Search", (hooks) => {
});
}
}
ToyView.components = { FavoriteMenu, SearchBar };
ToyView.template = xml`
ToyController.components = { FavoriteMenu, SearchBar };
ToyController.template = xml`
<div>
<SearchBar/>
<FavoriteMenu/>
</div>
`;
ToyView.type = "toy";
ToyView.display_name = "Toy";
viewRegistry.add("toy", ToyView);
viewRegistry.add("toy", {
type: "toy",
display_name: "Toy",
Controller: ToyController,
});
serverData.views["foo,false,toy"] = `<toy />`;
serverData.models.foo.filters = [
@@ -348,7 +348,7 @@ QUnit.module("Views", (hooks) => {
// this is important for export functionality.
assert.strictEqual(
pivot.model.metaData.title,
pivot.model.metaData.title.toString(),
pivot.env._t("Untitled"),
"should have a valid title"
);
+76 -78
View File
@@ -71,25 +71,29 @@ QUnit.module("Views", (hooks) => {
},
};
class ToyView extends Component {
class ToyController extends Component {
setup() {
this.class = "toy";
this.template = xml`${this.props.arch}`;
}
}
ToyView.template = xml`<div t-attf-class="{{class}} {{props.className}}"><t t-call="{{ template }}"/></div>`;
ToyView.type = "toy";
ToyView.components = { Banner: OnboardingBanner };
ToyController.template = xml`<div t-attf-class="{{class}} {{props.className}}"><t t-call="{{ template }}"/></div>`;
ToyController.components = { Banner: OnboardingBanner };
class ToyViewImp extends ToyView {
const toyView = {
type: "toy",
Controller: ToyController,
};
class ToyControllerImp extends ToyController {
setup() {
super.setup();
this.class = "toy_imp";
}
}
viewRegistry.add("toy", ToyView);
viewRegistry.add("toy_imp", ToyViewImp);
viewRegistry.add("toy", toyView);
viewRegistry.add("toy_imp", { ...toyView, Controller: ToyControllerImp });
setupControlPanelServiceRegistry();
@@ -115,8 +119,8 @@ QUnit.module("Views", (hooks) => {
QUnit.test("simple rendering", async function (assert) {
assert.expect(10);
const ToyView = viewRegistry.get("toy");
patchWithCleanup(ToyView.prototype, {
const ToyController = viewRegistry.get("toy").Controller;
patchWithCleanup(ToyController.prototype, {
setup() {
this._super();
const { arch, fields, info } = this.props;
@@ -153,8 +157,8 @@ QUnit.module("Views", (hooks) => {
QUnit.test("rendering with given viewId", async function (assert) {
assert.expect(8);
const ToyView = viewRegistry.get("toy");
patchWithCleanup(ToyView.prototype, {
const ToyController = viewRegistry.get("toy").Controller;
patchWithCleanup(ToyController.prototype, {
setup() {
this._super();
const { arch, fields, info } = this.props;
@@ -190,8 +194,8 @@ QUnit.module("Views", (hooks) => {
QUnit.test("rendering with given 'views' param", async function (assert) {
assert.expect(8);
const ToyView = viewRegistry.get("toy");
patchWithCleanup(ToyView.prototype, {
const ToyController = viewRegistry.get("toy").Controller;
patchWithCleanup(ToyController.prototype, {
setup() {
this._super();
const { arch, fields, info } = this.props;
@@ -231,8 +235,8 @@ QUnit.module("Views", (hooks) => {
async function (assert) {
assert.expect(8);
const ToyView = viewRegistry.get("toy");
patchWithCleanup(ToyView.prototype, {
const ToyController = viewRegistry.get("toy").Controller;
patchWithCleanup(ToyController.prototype, {
setup() {
this._super();
const { arch, fields, info } = this.props;
@@ -274,8 +278,8 @@ QUnit.module("Views", (hooks) => {
QUnit.test("viewId defined as prop and in 'views' prop", async function (assert) {
assert.expect(8);
const ToyView = viewRegistry.get("toy");
patchWithCleanup(ToyView.prototype, {
const ToyController = viewRegistry.get("toy").Controller;
patchWithCleanup(ToyController.prototype, {
setup() {
this._super();
const { arch, fields, info } = this.props;
@@ -320,8 +324,8 @@ QUnit.module("Views", (hooks) => {
QUnit.test("rendering with given arch and fields", async function (assert) {
assert.expect(6);
const ToyView = viewRegistry.get("toy");
patchWithCleanup(ToyView.prototype, {
const ToyController = viewRegistry.get("toy").Controller;
patchWithCleanup(ToyController.prototype, {
setup() {
this._super();
const { arch, fields, info } = this.props;
@@ -353,8 +357,8 @@ QUnit.module("Views", (hooks) => {
QUnit.test("rendering with loadActionMenus='true'", async function (assert) {
assert.expect(8);
const ToyView = viewRegistry.get("toy");
patchWithCleanup(ToyView.prototype, {
const ToyController = viewRegistry.get("toy").Controller;
patchWithCleanup(ToyController.prototype, {
setup() {
this._super();
const { arch, fields, info } = this.props;
@@ -393,8 +397,8 @@ QUnit.module("Views", (hooks) => {
async function (assert) {
assert.expect(8);
const ToyView = viewRegistry.get("toy");
patchWithCleanup(ToyView.prototype, {
const ToyController = viewRegistry.get("toy").Controller;
patchWithCleanup(ToyController.prototype, {
setup() {
this._super();
const { arch, fields, info } = this.props;
@@ -436,8 +440,8 @@ QUnit.module("Views", (hooks) => {
async function (assert) {
assert.expect(6);
const ToyView = viewRegistry.get("toy");
patchWithCleanup(ToyView.prototype, {
const ToyController = viewRegistry.get("toy").Controller;
patchWithCleanup(ToyController.prototype, {
setup() {
this._super();
const { arch, fields, info } = this.props;
@@ -474,8 +478,8 @@ QUnit.module("Views", (hooks) => {
QUnit.test("rendering with given searchViewId", async function (assert) {
assert.expect(8);
const ToyView = viewRegistry.get("toy");
patchWithCleanup(ToyView.prototype, {
const ToyController = viewRegistry.get("toy").Controller;
patchWithCleanup(ToyController.prototype, {
setup() {
this._super();
const {
@@ -522,8 +526,8 @@ QUnit.module("Views", (hooks) => {
async function (assert) {
assert.expect(6);
const ToyView = viewRegistry.get("toy");
patchWithCleanup(ToyView.prototype, {
const ToyController = viewRegistry.get("toy").Controller;
patchWithCleanup(ToyController.prototype, {
setup() {
this._super();
const {
@@ -566,8 +570,8 @@ QUnit.module("Views", (hooks) => {
async function (assert) {
assert.expect(6);
const ToyView = viewRegistry.get("toy");
patchWithCleanup(ToyView.prototype, {
const ToyController = viewRegistry.get("toy").Controller;
patchWithCleanup(ToyController.prototype, {
setup() {
this._super();
const {
@@ -609,8 +613,8 @@ QUnit.module("Views", (hooks) => {
async function (assert) {
assert.expect(8);
const ToyView = viewRegistry.get("toy");
patchWithCleanup(ToyView.prototype, {
const ToyController = viewRegistry.get("toy").Controller;
patchWithCleanup(ToyController.prototype, {
setup() {
this._super();
const {
@@ -674,8 +678,8 @@ QUnit.module("Views", (hooks) => {
},
];
const ToyView = viewRegistry.get("toy");
patchWithCleanup(ToyView.prototype, {
const ToyController = viewRegistry.get("toy").Controller;
patchWithCleanup(ToyController.prototype, {
setup() {
this._super();
const {
@@ -1018,14 +1022,18 @@ QUnit.module("Views", (hooks) => {
</div>`;
let toy;
const ToyView = viewRegistry.get("toy");
class ToyViewExtended extends ToyView {
const toyView = viewRegistry.get("toy");
class ToyViewExtendedController extends toyView.Controller {
setup() {
super.setup();
toy = this;
}
}
viewRegistry.add("toy", ToyViewExtended, { force: true });
viewRegistry.add(
"toy",
{ ...toyView, Controller: ToyViewExtendedController },
{ force: true }
);
const mockRPC = (route) => {
if (route === "/mybody/isacage") {
@@ -1407,7 +1415,7 @@ QUnit.module("Views", (hooks) => {
async function (assert) {
assert.expect(4);
class ToyView extends Component {
class ToyController extends Component {
setup() {
const { context, domain, groupBy, orderBy } = this.props;
assert.deepEqual(context, {
@@ -1421,10 +1429,9 @@ QUnit.module("Views", (hooks) => {
assert.deepEqual(orderBy, ["bar"]);
}
}
ToyView.template = xml`<div/>`;
ToyView.type = "toy";
ToyController.template = xml`<div/>`;
viewRegistry.add("toy", ToyView, { force: true });
viewRegistry.add("toy", { type: "toy", Controller: ToyController }, { force: true });
const env = await makeTestEnv({ serverData });
const props = {
@@ -1442,14 +1449,13 @@ QUnit.module("Views", (hooks) => {
QUnit.test("non empty prop 'noContentHelp'", async function (assert) {
assert.expect(1);
class ToyView extends Component {
class ToyController extends Component {
setup() {
assert.strictEqual(this.props.info.noContentHelp, "<div>Help</div>");
}
}
ToyView.template = xml`<div/>`;
ToyView.type = "toy";
viewRegistry.add("toy", ToyView, { force: true });
ToyController.template = xml`<div/>`;
viewRegistry.add("toy", { type: "toy", Controller: ToyController }, { force: true });
const env = await makeTestEnv({ serverData });
const props = {
@@ -1463,14 +1469,13 @@ QUnit.module("Views", (hooks) => {
QUnit.test("useSampleModel false by default", async function (assert) {
assert.expect(1);
class ToyView extends Component {
class ToyController extends Component {
setup() {
assert.strictEqual(this.props.useSampleModel, false);
}
}
ToyView.template = xml`<div/>`;
ToyView.type = "toy";
viewRegistry.add("toy", ToyView, { force: true });
ToyController.template = xml`<div/>`;
viewRegistry.add("toy", { type: "toy", Controller: ToyController }, { force: true });
const env = await makeTestEnv({ serverData });
const props = { resModel: "animal", type: "toy" };
@@ -1480,14 +1485,13 @@ QUnit.module("Views", (hooks) => {
QUnit.test("sample='1' on arch", async function (assert) {
assert.expect(1);
class ToyView extends Component {
class ToyController extends Component {
setup() {
assert.strictEqual(this.props.useSampleModel, true);
}
}
ToyView.template = xml`<div/>`;
ToyView.type = "toy";
viewRegistry.add("toy", ToyView, { force: true });
ToyController.template = xml`<div/>`;
viewRegistry.add("toy", { type: "toy", Controller: ToyController }, { force: true });
const env = await makeTestEnv({ serverData });
const props = {
@@ -1502,14 +1506,13 @@ QUnit.module("Views", (hooks) => {
QUnit.test("sample='0' on arch and useSampleModel=true", async function (assert) {
assert.expect(1);
class ToyView extends Component {
class ToyController extends Component {
setup() {
assert.strictEqual(this.props.useSampleModel, true);
}
}
ToyView.template = xml`<div/>`;
ToyView.type = "toy";
viewRegistry.add("toy", ToyView, { force: true });
ToyController.template = xml`<div/>`;
viewRegistry.add("toy", { type: "toy", Controller: ToyController }, { force: true });
const env = await makeTestEnv({ serverData });
const props = {
@@ -1525,14 +1528,13 @@ QUnit.module("Views", (hooks) => {
QUnit.test("sample='1' on arch and useSampleModel=false", async function (assert) {
assert.expect(1);
class ToyView extends Component {
class ToyController extends Component {
setup() {
assert.strictEqual(this.props.useSampleModel, false);
}
}
ToyView.template = xml`<div/>`;
ToyView.type = "toy";
viewRegistry.add("toy", ToyView, { force: true });
ToyController.template = xml`<div/>`;
viewRegistry.add("toy", { type: "toy", Controller: ToyController }, { force: true });
const env = await makeTestEnv({ serverData });
const props = {
@@ -1548,14 +1550,13 @@ QUnit.module("Views", (hooks) => {
QUnit.test("useSampleModel=true", async function (assert) {
assert.expect(1);
class ToyView extends Component {
class ToyController extends Component {
setup() {
assert.strictEqual(this.props.useSampleModel, true);
}
}
ToyView.template = xml`<div/>`;
ToyView.type = "toy";
viewRegistry.add("toy", ToyView, { force: true });
ToyController.template = xml`<div/>`;
viewRegistry.add("toy", { type: "toy", Controller: ToyController }, { force: true });
const env = await makeTestEnv({ serverData });
const props = { resModel: "animal", type: "toy", useSampleModel: true };
@@ -1565,14 +1566,13 @@ QUnit.module("Views", (hooks) => {
QUnit.test("rendering with given prop", async function (assert) {
assert.expect(1);
class ToyView extends Component {
class ToyController extends Component {
setup() {
assert.strictEqual(this.props.specificProp, "specificProp");
}
}
ToyView.template = xml`<div/>`;
ToyView.type = "toy";
viewRegistry.add("toy", ToyView, { force: true });
ToyController.template = xml`<div/>`;
viewRegistry.add("toy", { type: "toy", Controller: ToyController }, { force: true });
const env = await makeTestEnv({ serverData });
const props = { resModel: "animal", type: "toy", specificProp: "specificProp" };
@@ -1584,7 +1584,7 @@ QUnit.module("Views", (hooks) => {
async function (assert) {
assert.expect(4);
class ToyView extends Component {
class ToyController extends Component {
setup() {
const { context, domain, groupBy, orderBy } = this.props;
assert.deepEqual(context, {
@@ -1597,9 +1597,8 @@ QUnit.module("Views", (hooks) => {
assert.deepEqual(orderBy, ["bar"]);
}
}
ToyView.template = xml`<div/>`;
ToyView.type = "toy";
viewRegistry.add("toy", ToyView, { force: true });
ToyController.template = xml`<div/>`;
viewRegistry.add("toy", { type: "toy", Controller: ToyController }, { force: true });
const env = await makeTestEnv({ serverData });
const props = {
@@ -1622,7 +1621,7 @@ QUnit.module("Views", (hooks) => {
QUnit.test("react to prop 'domain' changes", async function (assert) {
assert.expect(2);
class ToyView extends Component {
class ToyController extends Component {
setup() {
onWillStart(() => {
assert.deepEqual(this.props.domain, [["type", "=", "carnivorous"]]);
@@ -1632,9 +1631,8 @@ QUnit.module("Views", (hooks) => {
});
}
}
ToyView.template = xml`<div/>`;
ToyView.type = "toy";
viewRegistry.add("toy", ToyView, { force: true });
ToyController.template = xml`<div/>`;
viewRegistry.add("toy", { type: "toy", Controller: ToyController }, { force: true });
const env = await makeTestEnv({ serverData });
@@ -12,7 +12,7 @@ import { createWebClient, doAction, getActionManagerServerData } from "./../help
import FormController from "web.FormController";
import { makeFakeUserService } from "@web/../tests/helpers/mock_services";
import ListController from "web.ListController";
import { PivotView } from "@web/views/pivot/pivot_view";
import { pivotView } from "@web/views/pivot/pivot_view";
import { registry } from "@web/core/registry";
const serviceRegistry = registry.category("services");
@@ -194,7 +194,7 @@ QUnit.module("ActionManager", (hooks) => {
async function (assert) {
assert.expect(7);
let pivot;
patchWithCleanup(PivotView.prototype, {
patchWithCleanup(pivotView.Controller.prototype, {
setup() {
this._super(...arguments);
pivot = this;
@@ -669,7 +669,7 @@ QUnit.module("ActionManager", (hooks) => {
let def = Promise.resolve();
let id = 1;
class ToyView extends Component {
class ToyController extends Component {
setup() {
this.id = id++;
assert.step(JSON.stringify(this.props.state || "no state"));
@@ -681,18 +681,20 @@ QUnit.module("ActionManager", (hooks) => {
owl.onWillStart(() => def);
}
}
ToyView.components = { ControlPanel };
ToyView.display_name = "Toy";
ToyView.icon = "fab fa-android";
ToyView.multiRecord = true;
ToyView.searchMenuTypes = ["filter"];
ToyView.template = xml`
ToyController.template = xml`
<div class="o_toy_view">
<ControlPanel />
</div>
`;
ToyView.type = "toy";
registry.category("views").add("toy", ToyView);
</div>`;
ToyController.components = { ControlPanel };
registry.category("views").add("toy", {
type: "toy",
display_name: "Toy",
icon: "fab fa-android",
multiRecord: true,
searchMenuTypes: ["filter"],
Controller: ToyController,
});
const webClient = await createWebClient({ serverData });