From b828cfc72c587d0b73fcc5459695705640437671 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Wed, 27 Apr 2022 13:35:10 +0000 Subject: [PATCH] [REF] web, *: improve the way views are defined, reintroduce controllers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../static/src/js/forecast/forecast_views.js | 22 ++- .../static/src/js/attendance_report_views.js | 18 +- .../static/src/js/timesheet_graph_view.js | 8 +- .../burndown_chart_pivot_view.js | 8 +- .../src/burndown_chart/burndown_chart_view.js | 14 +- .../static/src/js/project_graph_view.js | 7 +- .../static/src/js/project_pivot_view.js | 9 +- .../src/js/project_rating_graph_view.js | 11 +- .../src/js/project_rating_pivot_view.js | 10 +- .../src/search/with_search/with_search.js | 3 +- .../src/views/graph/graph_controller.js | 134 +++++++++++++ .../{graph_view.xml => graph_controller.xml} | 4 +- .../web/static/src/views/graph/graph_view.js | 176 +++-------------- .../src/views/pivot/pivot_controller.js | 145 ++++++++++++++ .../{pivot_view.xml => pivot_controller.xml} | 4 +- .../web/static/src/views/pivot/pivot_view.js | 184 +++--------------- addons/web/static/src/views/view.js | 16 +- .../tests/core/debug/debug_manager_tests.js | 13 +- .../tests/legacy/views/state_mapping_tests.js | 24 ++- .../tests/search/favorite_menu_tests.js | 14 +- .../static/tests/views/pivot_view_tests.js | 2 +- addons/web/static/tests/views/view_tests.js | 154 ++++++++------- .../webclient/actions/close_action_tests.js | 4 +- .../webclient/actions/concurrency_tests.js | 24 +-- 24 files changed, 529 insertions(+), 479 deletions(-) create mode 100644 addons/web/static/src/views/graph/graph_controller.js rename addons/web/static/src/views/graph/{graph_view.xml => graph_controller.xml} (97%) create mode 100644 addons/web/static/src/views/pivot/pivot_controller.js rename addons/web/static/src/views/pivot/{pivot_view.xml => pivot_controller.xml} (93%) diff --git a/addons/crm/static/src/js/forecast/forecast_views.js b/addons/crm/static/src/js/forecast/forecast_views.js index f1eac289a37..607510db57e 100644 --- a/addons/crm/static/src/js/forecast/forecast_views.js +++ b/addons/crm/static/src/js/forecast/forecast_views.js @@ -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, }; diff --git a/addons/hr_attendance/static/src/js/attendance_report_views.js b/addons/hr_attendance/static/src/js/attendance_report_views.js index 6de235aa274..07513fb7338 100644 --- a/addons/hr_attendance/static/src/js/attendance_report_views.js +++ b/addons/hr_attendance/static/src/js/attendance_report_views.js @@ -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 +}); diff --git a/addons/hr_timesheet/static/src/js/timesheet_graph_view.js b/addons/hr_timesheet/static/src/js/timesheet_graph_view.js index 155d4b17626..e6df6c7ad85 100644 --- a/addons/hr_timesheet/static/src/js/timesheet_graph_view.js +++ b/addons/hr_timesheet/static/src/js/timesheet_graph_view.js @@ -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); diff --git a/addons/project/static/src/burndown_chart/burndown_chart_pivot_view.js b/addons/project/static/src/burndown_chart/burndown_chart_pivot_view.js index 99d70fbab7a..52e4d96cf7d 100644 --- a/addons/project/static/src/burndown_chart/burndown_chart_pivot_view.js +++ b/addons/project/static/src/burndown_chart/burndown_chart_pivot_view.js @@ -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); diff --git a/addons/project/static/src/burndown_chart/burndown_chart_view.js b/addons/project/static/src/burndown_chart/burndown_chart_view.js index a3f727a29a5..e6d5c2b0f8d 100644 --- a/addons/project/static/src/burndown_chart/burndown_chart_view.js +++ b/addons/project/static/src/burndown_chart/burndown_chart_view.js @@ -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); diff --git a/addons/project/static/src/js/project_graph_view.js b/addons/project/static/src/js/project_graph_view.js index 416835d1a1f..7c3b7eda969 100644 --- a/addons/project/static/src/js/project_graph_view.js +++ b/addons/project/static/src/js/project_graph_view.js @@ -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); diff --git a/addons/project/static/src/js/project_pivot_view.js b/addons/project/static/src/js/project_pivot_view.js index a221d933405..5699d007ac8 100644 --- a/addons/project/static/src/js/project_pivot_view.js +++ b/addons/project/static/src/js/project_pivot_view.js @@ -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); diff --git a/addons/project/static/src/js/project_rating_graph_view.js b/addons/project/static/src/js/project_rating_graph_view.js index 380ef3ffb2b..00e3d9fde56 100644 --- a/addons/project/static/src/js/project_rating_graph_view.js +++ b/addons/project/static/src/js/project_rating_graph_view.js @@ -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); diff --git a/addons/project/static/src/js/project_rating_pivot_view.js b/addons/project/static/src/js/project_rating_pivot_view.js index e5dae9a110a..f921b30c338 100644 --- a/addons/project/static/src/js/project_rating_pivot_view.js +++ b/addons/project/static/src/js/project_rating_pivot_view.js @@ -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); diff --git a/addons/web/static/src/search/with_search/with_search.js b/addons/web/static/src/search/with_search/with_search.js index 046331cfb69..3b786f7166e 100644 --- a/addons/web/static/src/search/with_search/with_search.js +++ b/addons/web/static/src/search/with_search/with_search.js @@ -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, diff --git a/addons/web/static/src/views/graph/graph_controller.js b/addons/web/static/src/views/graph/graph_controller.js new file mode 100644 index 00000000000..b99260c23d2 --- /dev/null +++ b/addons/web/static/src/views/graph/graph_controller.js @@ -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, +}; diff --git a/addons/web/static/src/views/graph/graph_view.xml b/addons/web/static/src/views/graph/graph_controller.xml similarity index 97% rename from addons/web/static/src/views/graph/graph_view.xml rename to addons/web/static/src/views/graph/graph_controller.xml index 077ea4ea42e..0f5432bd93d 100644 --- a/addons/web/static/src/views/graph/graph_view.xml +++ b/addons/web/static/src/views/graph/graph_controller.xml @@ -49,13 +49,13 @@
- + - { 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); diff --git a/addons/web/static/src/views/pivot/pivot_controller.js b/addons/web/static/src/views/pivot/pivot_controller.js new file mode 100644 index 00000000000..dc4b82329da --- /dev/null +++ b/addons/web/static/src/views/pivot/pivot_controller.js @@ -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: [], +}; diff --git a/addons/web/static/src/views/pivot/pivot_view.xml b/addons/web/static/src/views/pivot/pivot_controller.xml similarity index 93% rename from addons/web/static/src/views/pivot/pivot_view.xml rename to addons/web/static/src/views/pivot/pivot_controller.xml index 0a7ffe8f11a..11c6f04d564 100644 --- a/addons/web/static/src/views/pivot/pivot_view.xml +++ b/addons/web/static/src/views/pivot/pivot_controller.xml @@ -20,7 +20,7 @@
- + - +
diff --git a/addons/web/static/src/views/pivot/pivot_view.js b/addons/web/static/src/views/pivot/pivot_view.js index a039dfa0772..d8ea9e95521 100644 --- a/addons/web/static/src/views/pivot/pivot_view.js +++ b/addons/web/static/src/views/pivot/pivot_view.js @@ -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); diff --git a/addons/web/static/src/views/view.js b/addons/web/static/src/views/view.js index 7f85bd9fc4a..67462af3ae9 100644 --- a/addons/web/static/src/views/view.js +++ b/addons/web/static/src/views/view.js @@ -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; } diff --git a/addons/web/static/tests/core/debug/debug_manager_tests.js b/addons/web/static/tests/core/debug/debug_manager_tests.js index 6a04a5fd2cc..e37d49f4435 100644 --- a/addons/web/static/tests/core/debug/debug_manager_tests.js +++ b/addons/web/static/tests/core/debug/debug_manager_tests.js @@ -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`
`; - ToyView.type = "toy"; - ToyView.display_name = "toy view"; - registry.category("views").add("toy", ToyView); + ToyController.template = xml`
`; + + registry.category("views").add("toy", { + type: "toy", + display_name: "toy view", + Controller: ToyController, + }); const mockRPC = async (route, args) => { if (args.method === "check_access_rights") { diff --git a/addons/web/static/tests/legacy/views/state_mapping_tests.js b/addons/web/static/tests/legacy/views/state_mapping_tests.js index d6399e705d9..fda85b64a5a 100644 --- a/addons/web/static/tests/legacy/views/state_mapping_tests.js +++ b/addons/web/static/tests/legacy/views/state_mapping_tests.js @@ -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` -
- -
- `; - ToyView.type = "toy"; + class ToyController extends LegacyComponent {} + ToyController.template = xml`
`; + 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); }); diff --git a/addons/web/static/tests/search/favorite_menu_tests.js b/addons/web/static/tests/search/favorite_menu_tests.js index 379c5ed0acf..e85903d6275 100644 --- a/addons/web/static/tests/search/favorite_menu_tests.js +++ b/addons/web/static/tests/search/favorite_menu_tests.js @@ -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`
`; - 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"] = ``; serverData.models.foo.filters = [ diff --git a/addons/web/static/tests/views/pivot_view_tests.js b/addons/web/static/tests/views/pivot_view_tests.js index 46b5e519f4b..8f2d471f7f4 100644 --- a/addons/web/static/tests/views/pivot_view_tests.js +++ b/addons/web/static/tests/views/pivot_view_tests.js @@ -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" ); diff --git a/addons/web/static/tests/views/view_tests.js b/addons/web/static/tests/views/view_tests.js index fa15569ae21..5a694a025c1 100644 --- a/addons/web/static/tests/views/view_tests.js +++ b/addons/web/static/tests/views/view_tests.js @@ -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`
`; - ToyView.type = "toy"; - ToyView.components = { Banner: OnboardingBanner }; + ToyController.template = xml`
`; + 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) => {
`; 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`
`; - ToyView.type = "toy"; + ToyController.template = xml`
`; - 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, "
Help
"); } } - ToyView.template = xml`
`; - ToyView.type = "toy"; - viewRegistry.add("toy", ToyView, { force: true }); + ToyController.template = xml`
`; + 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`
`; - ToyView.type = "toy"; - viewRegistry.add("toy", ToyView, { force: true }); + ToyController.template = xml`
`; + 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`
`; - ToyView.type = "toy"; - viewRegistry.add("toy", ToyView, { force: true }); + ToyController.template = xml`
`; + 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`
`; - ToyView.type = "toy"; - viewRegistry.add("toy", ToyView, { force: true }); + ToyController.template = xml`
`; + 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`
`; - ToyView.type = "toy"; - viewRegistry.add("toy", ToyView, { force: true }); + ToyController.template = xml`
`; + 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`
`; - ToyView.type = "toy"; - viewRegistry.add("toy", ToyView, { force: true }); + ToyController.template = xml`
`; + 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`
`; - ToyView.type = "toy"; - viewRegistry.add("toy", ToyView, { force: true }); + ToyController.template = xml`
`; + 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`
`; - ToyView.type = "toy"; - viewRegistry.add("toy", ToyView, { force: true }); + ToyController.template = xml`
`; + 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`
`; - ToyView.type = "toy"; - viewRegistry.add("toy", ToyView, { force: true }); + ToyController.template = xml`
`; + viewRegistry.add("toy", { type: "toy", Controller: ToyController }, { force: true }); const env = await makeTestEnv({ serverData }); diff --git a/addons/web/static/tests/webclient/actions/close_action_tests.js b/addons/web/static/tests/webclient/actions/close_action_tests.js index 77d38da3fd4..a852e580d89 100644 --- a/addons/web/static/tests/webclient/actions/close_action_tests.js +++ b/addons/web/static/tests/webclient/actions/close_action_tests.js @@ -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; diff --git a/addons/web/static/tests/webclient/actions/concurrency_tests.js b/addons/web/static/tests/webclient/actions/concurrency_tests.js index 31292228366..2bfb5af3048 100644 --- a/addons/web/static/tests/webclient/actions/concurrency_tests.js +++ b/addons/web/static/tests/webclient/actions/concurrency_tests.js @@ -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`
-
- `; - ToyView.type = "toy"; - registry.category("views").add("toy", ToyView); +
`; + 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 });