[IMP] web: move views' buttons into the view itself

Were previously in the ControlPanel.

Part-of: odoo/odoo#116641
This commit is contained in:
Romeo Fragomeli
2023-05-12 22:59:17 +02:00
committed by Pierre Paridans
parent caef16ee4e
commit c8ca9da7bc
23 changed files with 268 additions and 341 deletions
@@ -3,7 +3,7 @@
<t t-name="ImportAction" owl="1">
<div class="h-100 d-flex flex-column">
<Layout className="'o_import_action d-flex h-100 overflow-auto bg-white'" display="display">
<t t-set-slot="layout-buttons">
<t t-set-slot="control-panel-create-button">
<t t-if="isPreviewing">
<button t-if="!state.isPaused" type="button" class="btn btn-primary m-1" t-on-click="() => this.handleImport(false)">Import</button>
<button t-else="" type="button" class="btn btn-primary m-1" t-on-click="() => this.handleImport(false)">Resume</button>
@@ -20,13 +20,10 @@
</div>
</div>
</DatePicker>
</t>
<t t-name="calendar.AttendeeCalendarController.controlButtons" t-inherit="web.CalendarController.controlButtons" owl="1">
<xpath expr="//span[hasclass('o_calendar_navigation_buttons')]" position="before">
<span class="o_calendar_create_buttons">
<xpath expr="//Layout" position="inside">
<t t-set-slot="control-panel-create-button">
<button class="btn btn-primary o-calendar-button-new me-1" t-on-click="onClickAddButton">New</button>
</span>
</t>
</xpath>
</t>
</templates>
@@ -11,7 +11,6 @@ export const attendeeCalendarView = {
Controller: AttendeeCalendarController,
Model: AttendeeCalendarModel,
Renderer: AttendeeCalendarRenderer,
buttonTemplate: "calendar.AttendeeCalendarController.controlButtons",
};
registry.category("views").add("attendee_calendar", attendeeCalendarView);
@@ -111,7 +111,6 @@ export class TimeOffCalendarController extends CalendarController {
});
}
}
TimeOffCalendarController.template = "hr_holidays.CalendarController";
TimeOffCalendarController.components = {
...TimeOffCalendarController.components,
FilterPanel: TimeOffCalendarFilterPanel,
@@ -1,12 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="hr_holidays.CalendarController" t-inherit="web.CalendarController" t-inherit-mode="primary" owl="1">
<xpath expr="//DatePicker" position="replace"/>
</t>
<t t-name="hr_holidays.CalendarController.controlButtons" t-inherit="web.CalendarController.controlButtons" t-inherit-mode="primary" owl="1">
<xpath expr="//ViewScaleSelector" position="after">
<xpath expr="." position="inside">
<span class="o_timeoff_buttons">
<button class="btn btn-primary btn-time-off mx-1" t-on-click="newTimeOffRequest" type="button">
New Time Off
@@ -2,7 +2,7 @@
<templates>
<t t-name="hr_work_entry_contract.calendar.controlButtons" t-inherit="web.CalendarController.controlButtons" t-inherit-mode="primary" owl="1">
<xpath expr="//ViewScaleSelector" position="after">
<xpath expr="." position="inside">
<span class="btn-regenerate-work-entries">
<button class="btn btn-secondary ms-3" t-on-click="onRegenerateWorkEntries">
Regenerate Work Entries
@@ -5,8 +5,9 @@
<div class="o_control_panel d-flex flex-column gap-3 gap-lg-1 px-3 pt-2 pb-3" t-ref="root">
<div class="o_control_panel_main d-flex flex-wrap flex-lg-nowrap justify-content-between align-items-lg-start gap-3">
<div class="o_control_panel_breadcrumbs d-flex align-items-center w-100 w-md-50 w-lg-auto">
<div class="o_control_panel_create_button d-empty-none d-print-none me-2">
<div class="o_control_panel_main_buttons d-flex gap-1 d-empty-none d-print-none me-2">
<t t-slot="control-panel-create-button"/>
<t t-slot="layout-buttons"/>
<t t-slot="control-panel-always-buttons"/>
</div>
<t t-if="env.config.noBreadcrumbs">
@@ -43,9 +44,6 @@
</nav>
</div>
</div>
<div class="o_control_panel_secondary d-flex gap-2">
<t t-slot="layout-buttons"/>
</div>
</div>
</t>
@@ -49,13 +49,6 @@ export class CalendarCommonRenderer extends Component {
useEffect(() => {
this.updateSize();
});
useEffect(
(view) => {
this.env.config.setDisplayName(`${this.props.displayName} (${view.title})`);
},
() => [this.fc.api.view]
);
}
get options() {
@@ -1,12 +1,11 @@
/** @odoo-module **/
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
import { _lt, _t } from "@web/core/l10n/translation";
import { _t, _lt } from "@web/core/l10n/translation";
import { useOwnedDialogs, useService } from "@web/core/utils/hooks";
import { sprintf } from "@web/core/utils/strings";
import { Layout } from "@web/search/layout";
import { useModel } from "@web/views/model";
import { ViewScaleSelector } from "@web/views/view_components/view_scale_selector";
import { FormViewDialog } from "@web/views/view_dialogs/form_view_dialog";
import { useSetupView } from "@web/views/view_hook";
import { CalendarDatePicker } from "./date_picker/calendar_date_picker";
@@ -14,6 +13,7 @@ import { CalendarFilterPanel } from "./filter_panel/calendar_filter_panel";
import { CalendarMobileFilterPanel } from "./mobile_filter_panel/calendar_mobile_filter_panel";
import { CalendarQuickCreate } from "./quick_create/calendar_quick_create";
import { SearchBar } from "@web/search/search_bar/search_bar";
import { ViewScaleSelector } from "@web/views/view_components/view_scale_selector";
import { Component, useState } from "@odoo/owl";
@@ -64,7 +64,6 @@ export class CalendarController extends Component {
createRecord: this.createRecord.bind(this),
deleteRecord: this.deleteRecord.bind(this),
editRecord: this.editRecord.bind(this),
setDate: this.setDate.bind(this),
displayName: this.displayName,
};
}
@@ -90,11 +89,7 @@ export class CalendarController extends Component {
toggleSideBar: () => (this.state.showSideBar = !this.state.showSideBar),
};
}
get scales() {
return Object.fromEntries(
this.model.scales.map((s) => [s, { description: SCALE_LABELS[s] }])
);
}
get showCalendar() {
return !this.env.isSmall || !this.state.showSideBar;
}
@@ -107,29 +102,6 @@ export class CalendarController extends Component {
return this.props.className;
}
getTodayDay() {
return luxon.DateTime.local().day;
}
async setDate(move) {
let date = null;
switch (move) {
case "next":
date = this.model.date.plus({ [`${this.model.scale}s`]: 1 });
break;
case "previous":
date = this.model.date.minus({ [`${this.model.scale}s`]: 1 });
break;
case "today":
date = luxon.DateTime.local().startOf("day");
break;
}
await this.model.load({ date });
}
async setScale(scale) {
await this.model.load({ scale });
}
getQuickCreateProps(record) {
return {
record,
@@ -214,6 +186,31 @@ export class CalendarController extends Component {
},
});
}
async setDate(move) {
let date = null;
switch (move) {
case "next":
date = this.model.date.plus({ [`${this.model.scale}s`]: 1 });
break;
case "previous":
date = this.model.date.minus({ [`${this.model.scale}s`]: 1 });
break;
case "today":
date = luxon.DateTime.local().startOf("day");
break;
}
await this.model.load({ date });
}
get scales() {
return Object.fromEntries(
this.model.scales.map((s) => [s, { description: SCALE_LABELS[s] }])
);
}
async setScale(scale) {
await this.model.load({ scale });
}
}
CalendarController.components = {
DatePicker: CalendarDatePicker,
@@ -221,7 +218,7 @@ CalendarController.components = {
MobileFilterPanel: CalendarMobileFilterPanel,
QuickCreate: CalendarQuickCreate,
Layout,
ViewScaleSelector,
SearchBar,
ViewScaleSelector,
};
CalendarController.template = "web.CalendarController";
@@ -13,6 +13,15 @@
<div class="o_calendar_container">
<MobileFilterPanel t-if="env.isSmall" t-props="mobileFilterPanelProps" />
<div t-if="showSideBar" class="o_calendar_sidebar_container d-md-block">
<div class="d-flex justify-content-between">
<button
class="btn btn-secondary mx-1 o_calendar_button_today"
t-on-click.stop="() => this.setDate('today')"
>
Today
</button>
<ViewScaleSelector scales="scales" currentScale="model.scale" setScale.bind="setScale" />
</div>
<div class="o_calendar_sidebar">
<DatePicker t-if="!env.isSmall" t-props="datePickerProps" />
<FilterPanel t-props="filterPanelProps" />
@@ -24,45 +33,6 @@
</div>
</t>
<t t-name="web.CalendarController.controlButtons" owl="1">
<div class="o_calendar_buttons d-flex align-items-center mb-2">
<ViewScaleSelector scales="scales" currentScale="model.scale" setScale.bind="setScale" />
<span t-if="!env.isSmall" class="o_calendar_navigation_buttons mx-1">
<button
class="btn btn-secondary mx-1 o_calendar_button_today"
t-on-click.stop="() => this.setDate('today')"
>
Today
</button>
<button
class="btn btn-text pe-2 o_calendar_button_prev"
title="Previous"
aria-label="Previous"
t-on-click.stop="() => this.setDate('previous')"
>
<i class="fa oi-larger fa-angle-left" />
</button>
<button
class="btn btn-text ps-1 o_calendar_button_next"
title="Next"
aria-label="Next"
t-on-click.stop="() => this.setDate('next')"
>
<i class="fa oi-larger fa-angle-right" />
</button>
<t t-log="this"></t>
</span>
<!-- POC -->
<span class="fs-3">April 2023</span>
<button t-if="env.isSmall" class="o_cp_today_button btn btn-sm btn-link" t-on-click="() => this.setDate('today')">
<span class="fa-stack o_calendar_button_today">
<i class="fa fa-calendar-o fa-stack-2x" role="img" aria-label="Today" title="Today" />
<strong class="o_calendar_text fa-stack-1x" t-esc="getTodayDay()" />
</span>
</button>
</div>
</t>
<t t-name="web.CalendarController.controlButtons" owl="1"/>
</templates>
@@ -1,13 +1,12 @@
/** @odoo-module **/
import { formatDate } from "@web/core/l10n/dates";
import { localization } from "@web/core/l10n/localization";
import { useDebounced } from "@web/core/utils/timing";
import { getColor } from "../colors";
import { useCalendarPopover, useFullCalendar } from "../hooks";
import { CalendarYearPopover } from "./calendar_year_popover";
import { Component, useEffect, useRef, onRendered } from "@odoo/owl";
import { Component, useEffect, useRef } from "@odoo/owl";
export class CalendarYearRenderer extends Component {
setup() {
@@ -26,11 +25,6 @@ export class CalendarYearRenderer extends Component {
useEffect(() => {
this.updateSize();
});
onRendered(() => {
const year = formatDate(this.props.model.date, { format: "yyyy" });
this.env.config.setDisplayName(`${this.props.displayName} (${year})`);
});
}
get options() {
@@ -1,8 +1,5 @@
/** @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 { useModel } from "@web/views/model";
@@ -14,7 +11,6 @@ import { Component, useRef } from "@odoo/owl";
export class GraphController extends Component {
setup() {
this.actionService = useService("action");
this.model = useModel(this.props.Model, this.props.modelParams);
useSetupView({
@@ -46,90 +42,10 @@ export class GraphController extends Component {
}
return context;
}
/**
* 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 });
}
toggleCumulated() {
const { cumulated } = this.model.metaData;
this.model.updateMetaData({ cumulated: !cumulated });
}
}
GraphController.template = "web.GraphView";
GraphController.components = { Dropdown, DropdownItem, Layout, GroupByMenu, SearchBar };
GraphController.components = { Layout, GroupByMenu, SearchBar };
GraphController.props = {
...standardViewProps,
@@ -53,9 +53,6 @@
<t t-name="web.GraphView" owl="1">
<div t-att-class="props.className" t-ref="root">
<Layout className="model.useSampleModel ? 'o_view_sample_data' : ''" display="props.display">
<t t-set-slot="layout-buttons">
<t t-call="{{ props.buttonTemplate }}"/>
</t>
<t t-set-slot="layout-actions">
<SearchBar/>
</t>
@@ -63,7 +60,7 @@
<t t-if="model.useSampleModel and props.info.noContentHelp" t-call="web.ActionHelper">
<t t-set="noContentHelp" t-value="props.info.noContentHelp"/>
</t>
<t t-component="props.Renderer" model="model" onGraphClicked.bind="onGraphClicked" />
<t t-component="props.Renderer" model="model" buttonTemplate="props.buttonTemplate" />
</t>
<t t-else="" t-call="web.NoContentHelper">
<t t-set="title">Invalid data</t>
@@ -10,6 +10,8 @@ import { renderToString } from "@web/core/utils/render";
import { useService } from "@web/core/utils/hooks";
import { Component, onWillUnmount, useEffect, useRef, onWillStart } from "@odoo/owl";
import { Dropdown } from "@web/core/dropdown/dropdown";
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
const NO_DATA = _lt("No data");
@@ -49,6 +51,7 @@ export class GraphRenderer extends Component {
this.canvasRef = useRef("canvas");
this.containerRef = useRef("container");
this.cookies = useService("cookie");
this.actionService = useService("action");
this.chart = null;
this.tooltip = null;
@@ -450,15 +453,8 @@ export class GraphRenderer extends Component {
* @returns {Object}
*/
getScaleOptions() {
const {
allIntegers,
fields,
groupBy,
measure,
measures,
mode,
stacked,
} = this.model.metaData;
const { allIntegers, fields, groupBy, measure, measures, mode, stacked } =
this.model.metaData;
if (mode === "pie") {
return {};
}
@@ -558,7 +554,7 @@ export class GraphRenderer extends Component {
const { _datasetIndex, _index } = activeElement;
const { domains } = this.chart.data.datasets[_datasetIndex];
if (domains) {
this.props.onGraphClicked(domains[_index]);
this.onGraphClickedFinal(domains[_index]);
}
}
@@ -674,7 +670,88 @@ export class GraphRenderer extends Component {
// advancing the animation service.
Chart.animationService.advance();
}
/**
* 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
*/
onGraphClickedFinal(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 });
}
toggleCumulated() {
const { cumulated } = this.model.metaData;
this.model.updateMetaData({ cumulated: !cumulated });
}
}
GraphRenderer.template = "web.GraphRenderer";
GraphRenderer.props = ["class?", "model", "onGraphClicked"];
GraphRenderer.components = { Dropdown, DropdownItem };
GraphRenderer.props = ["class?", "model", "buttonTemplate"];
@@ -28,8 +28,11 @@
</t>
<t t-name="web.GraphRenderer" owl="1">
<div t-att-class="'o_graph_renderer o_renderer h-100 ' + props.class" t-ref="root">
<div class="o_graph_canvas_container h-100 position-relative" t-ref="container">
<div t-att-class="'o_graph_renderer o_renderer h-100 d-flex flex-column border-top ' + props.class" t-ref="root">
<div class="d-flex gap-1 mt-2 mx-3 mb-3">
<t t-call="{{ props.buttonTemplate }}"/>
</div>
<div class="o_graph_canvas_container flex-grow-1 position-relative" t-ref="container">
<canvas t-ref="canvas" />
</div>
</div>
@@ -10,7 +10,10 @@
New
</button>
</t>
</t>
</t>
<t t-set-slot="layout-buttons">
<t t-call="{{ props.buttonTemplate }}"/>
</t>
<t t-set-slot="control-panel-always-buttons">
<t t-foreach="headerButtons" t-as="button" t-key="button.id">
<MultiRecordViewButton
@@ -26,9 +29,6 @@
/>
</t>
</t>
<t t-set-slot="layout-buttons">
<t t-call="{{ props.buttonTemplate }}"/>
</t>
<t t-set-slot="layout-actions">
<SearchBar/>
</t>
@@ -12,6 +12,9 @@
</t>
</t>
<t t-set-slot="layout-buttons">
<t t-call="{{ props.buttonTemplate }}"/>
</t>
<t t-set-slot="control-panel-always-buttons">
<t t-foreach="archInfo.headerButtons" t-as="button" t-key="button.id">
<MultiRecordViewButton
@@ -28,12 +31,6 @@
</t>
</t>
<t t-set-slot="layout-buttons">
<div class="o_cp_buttons" role="toolbar" aria-label="Control panel buttons" t-ref="buttons">
<t t-call="{{ props.buttonTemplate }}"/>
</div>
</t>
<t t-set-slot="layout-actions">
<SearchBar/>
</t>
@@ -1,9 +1,5 @@
/** @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/model";
import { standardViewProps } from "@web/views/standard_view_props";
@@ -14,7 +10,6 @@ import { Component, useRef } from "@odoo/owl";
export class PivotController extends Component {
setup() {
this.actionService = useService("action");
this.model = useModel(this.props.Model, this.props.modelParams);
useSetupView({
@@ -36,108 +31,14 @@ export class PivotController extends Component {
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, SearchBar };
PivotController.components = { Layout, SearchBar };
PivotController.props = {
...standardViewProps,
Model: Function,
modelParams: Object,
Renderer: Function,
buttonTemplate: String,
};
@@ -2,26 +2,25 @@
<templates xml:space="preserve">
<t t-name="web.PivotView.Buttons" owl="1">
<div class="btn-group" role="toolbar" aria-label="Main actions">
<t t-call="web.ReportViewMeasures">
<t t-set="measures" t-value="model.metaData.measures"/>
<t t-set="activeMeasures" t-value="model.metaData.activeMeasures"/>
</t>
</div>
<div class="btn-group" role="toolbar" aria-label="Pivot settings">
<t t-set="noDataDisplayed" t-value="!model.hasData() || !model.metaData.activeMeasures.length"/>
<button class="btn btn-secondary fa fa-exchange o_pivot_flip_button" t-on-click="onFlipButtonClicked" data-tooltip="Flip axis" aria-label="Flip axis" t-attf-disabled="{{noDataDisplayed ? 'disabled' : false}}"/>
<button class="btn btn-secondary fa fa-arrows o_pivot_expand_button" t-on-click="onExpandButtonClicked" data-tooltip="Expand all" aria-label="Expand all" t-attf-disabled="{{noDataDisplayed ? 'disabled' : false}}"/>
<button class="btn btn-secondary fa fa-download o_pivot_download" t-on-click="onDownloadButtonClicked" data-tooltip="Download xlsx" aria-label="Download xlsx" t-attf-disabled="{{noDataDisplayed ? 'disabled' : false}}"/>
<div class="d-flex gap-1 mt-2 mx-3 mb-3">
<div class="btn-group" role="toolbar" aria-label="Main actions">
<t t-call="web.ReportViewMeasures">
<t t-set="measures" t-value="model.metaData.measures"/>
<t t-set="activeMeasures" t-value="model.metaData.activeMeasures"/>
</t>
</div>
<div class="btn-group" role="toolbar" aria-label="Pivot settings">
<t t-set="noDataDisplayed" t-value="!model.hasData() || !model.metaData.activeMeasures.length"/>
<button class="btn btn-secondary fa fa-exchange o_pivot_flip_button" t-on-click="onFlipButtonClicked" data-tooltip="Flip axis" aria-label="Flip axis" t-attf-disabled="{{noDataDisplayed ? 'disabled' : false}}"/>
<button class="btn btn-secondary fa fa-arrows o_pivot_expand_button" t-on-click="onExpandButtonClicked" data-tooltip="Expand all" aria-label="Expand all" t-attf-disabled="{{noDataDisplayed ? 'disabled' : false}}"/>
<button class="btn btn-secondary fa fa-download o_pivot_download" t-on-click="onDownloadButtonClicked" data-tooltip="Download xlsx" aria-label="Download xlsx" t-attf-disabled="{{noDataDisplayed ? 'disabled' : false}}"/>
</div>
</div>
</t>
<t t-name="web.PivotView" owl="1">
<div t-att-class="props.className" t-ref="root">
<Layout className="model.useSampleModel ? 'o_view_sample_data' : ''" display="props.display">
<t t-set-slot="layout-buttons">
<t t-call="{{ props.buttonTemplate }}"/>
</t>
<t t-set-slot="layout-actions">
<SearchBar/>
</t>
@@ -38,7 +37,7 @@
<t t-else="" t-call="web.NoContentHelper"/>
</t>
<t t-if="model.hasData() and model.metaData.activeMeasures.length">
<t t-component="props.Renderer" model="model" onCellClicked="cell => this.onOpenView(cell)"/>
<t t-component="props.Renderer" model="model"/>
</t>
</Layout>
</div>
@@ -3,15 +3,20 @@
import { CheckBox } from "@web/core/checkbox/checkbox";
import { localization } from "@web/core/l10n/localization";
import { registry } from "@web/core/registry";
import { Dropdown } from "@web/core/dropdown/dropdown";
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
import { formatPercentage } from "@web/views/fields/formatters";
import { PivotGroupByMenu } from "@web/views/pivot/pivot_group_by_menu";
import fieldUtils from "web.field_utils";
import { Component, onWillUpdateProps, useRef } from "@odoo/owl";
import { download } from "@web/core/network/download";
import { useService } from "@web/core/utils/hooks";
const formatters = registry.category("formatters");
export class PivotRenderer extends Component {
setup() {
this.actionService = useService("action");
this.model = this.props.model;
this.table = this.model.getTable();
this.l10n = localization;
@@ -71,9 +76,6 @@ export class PivotRenderer extends Component {
// Handlers
//----------------------------------------------------------------------
onCellClicked(cell) {
this.props.onCellClicked(cell);
}
/**
* Handle the adding of a custom groupby (inside the view, not the searchview).
*
@@ -148,7 +150,100 @@ export class PivotRenderer extends Component {
.querySelectorAll(".o_cell_hover")
.forEach((elt) => elt.classList.remove("o_cell_hover"));
}
//--------------------------------------------------------------------------
// 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.model.metaData.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);
}
}
PivotRenderer.template = "web.PivotRenderer";
PivotRenderer.components = { CheckBox, PivotGroupByMenu };
PivotRenderer.props = ["model", "onCellClicked"];
PivotRenderer.components = { Dropdown, DropdownItem, CheckBox, PivotGroupByMenu };
PivotRenderer.props = ["model"];
@@ -2,6 +2,7 @@
<templates xml:space="preserve">
<t t-name="web.PivotRenderer" owl="1">
<t t-call="web.PivotView.Buttons"/>
<div class="o_pivot table-responsive">
<table
@@ -31,7 +32,7 @@
o_empty: cell.value === undefined,
'cursor-pointer': cell.value !== undefined,
'fw-bold': cell.isBold,
}" t-on-click="() => this.onCellClicked(cell)"
}" t-on-click="() => this.onOpenView(cell)"
t-on-mouseover="onMouseEnter" t-on-mouseout="onMouseLeave">
<t t-if="cell.value !== undefined">
<div t-if="cell.originIndexes.length > 1" class="o_variation" t-att-class="{
@@ -21,7 +21,6 @@ export const pivotView = {
ArchParser: PivotArchParser,
SearchModel: PivotSearchModel,
searchMenuTypes: ["filter", "groupBy", "comparison", "favorite"],
buttonTemplate: "web.PivotView.Buttons",
props: (genericProps, view) => {
const modelParams = {};
@@ -57,7 +56,6 @@ export const pivotView = {
Model: view.Model,
modelParams,
Renderer: view.Renderer,
buttonTemplate: view.buttonTemplate,
};
},
};
@@ -28,7 +28,7 @@
</t>
</t>
</xpath>
<xpath expr="//Layout/t[@t-set-slot='layout-buttons']" position="replace">
<xpath expr="//Layout" position="inside">
<t t-set-slot="layout-buttons">
<t t-call="{{ props.buttonTemplate }}"/>
</t>