mirror of
https://github.com/bringout/oca-ocb-report.git
synced 2026-04-21 06:22:09 +02:00
Initial commit: Report packages
This commit is contained in:
commit
bc5e1e9efa
604 changed files with 474102 additions and 0 deletions
|
|
@ -0,0 +1,147 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { ControlPanel } from "@web/search/control_panel/control_panel";
|
||||
import { DashboardLoader, Status } from "./dashboard_loader";
|
||||
import spreadsheet from "@spreadsheet/o_spreadsheet/o_spreadsheet_extended";
|
||||
import { useSetupAction } from "@web/webclient/actions/action_hook";
|
||||
import { DashboardMobileSearchPanel } from "./mobile_search_panel/mobile_search_panel";
|
||||
import { MobileFigureContainer } from "./mobile_figure_container/mobile_figure_container";
|
||||
import { FilterValue } from "@spreadsheet/global_filters/components/filter_value/filter_value";
|
||||
import { loadSpreadsheetDependencies } from "@spreadsheet/helpers/helpers";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
const { Spreadsheet } = spreadsheet;
|
||||
const { Component, onWillStart, useState, useEffect } = owl;
|
||||
|
||||
export class SpreadsheetDashboardAction extends Component {
|
||||
setup() {
|
||||
this.Status = Status;
|
||||
this.controlPanelDisplay = {
|
||||
"top-left": true,
|
||||
"top-right": true,
|
||||
"bottom-left": false,
|
||||
"bottom-right": false,
|
||||
};
|
||||
this.orm = useService("orm");
|
||||
this.router = useService("router");
|
||||
// Use the non-protected orm service (`this.env.services.orm` instead of `useService("orm")`)
|
||||
// because spreadsheets models are preserved across multiple components when navigating
|
||||
// with the breadcrumb
|
||||
// TODO write a test
|
||||
/** @type {DashboardLoader}*/
|
||||
this.loader = useState(
|
||||
new DashboardLoader(this.env, this.env.services.orm, this._fetchDashboardData)
|
||||
);
|
||||
onWillStart(async () => {
|
||||
await loadSpreadsheetDependencies();
|
||||
if (this.props.state && this.props.state.dashboardLoader) {
|
||||
const { groups, dashboards } = this.props.state.dashboardLoader;
|
||||
this.loader.restoreFromState(groups, dashboards);
|
||||
} else {
|
||||
await this.loader.load();
|
||||
}
|
||||
const activeDashboardId = this.getInitialActiveDashboard();
|
||||
if (activeDashboardId) {
|
||||
this.openDashboard(activeDashboardId);
|
||||
}
|
||||
});
|
||||
useEffect(
|
||||
() => this.router.pushState({ dashboard_id: this.activeDashboardId }),
|
||||
() => [this.activeDashboardId]
|
||||
);
|
||||
useEffect(
|
||||
() => {
|
||||
const dashboard = this.state.activeDashboard;
|
||||
if (dashboard && dashboard.status === Status.Loaded) {
|
||||
const render = () => this.render(true);
|
||||
dashboard.model.on("update", this, render);
|
||||
return () => dashboard.model.off("update", this, render);
|
||||
}
|
||||
},
|
||||
() => {
|
||||
const dashboard = this.state.activeDashboard;
|
||||
return [dashboard && dashboard.model, dashboard && dashboard.status];
|
||||
}
|
||||
);
|
||||
useSetupAction({
|
||||
getLocalState: () => {
|
||||
return {
|
||||
activeDashboardId: this.activeDashboardId,
|
||||
dashboardLoader: this.loader.getState(),
|
||||
};
|
||||
},
|
||||
});
|
||||
/** @type {{ activeDashboard: import("./dashboard_loader").Dashboard}} */
|
||||
this.state = useState({ activeDashboard: undefined });
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {number | undefined}
|
||||
*/
|
||||
get activeDashboardId() {
|
||||
return this.state.activeDashboard ? this.state.activeDashboard.id : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {object[]}
|
||||
*/
|
||||
get filters() {
|
||||
const dashboard = this.state.activeDashboard;
|
||||
if (!dashboard || dashboard.status !== Status.Loaded) {
|
||||
return [];
|
||||
}
|
||||
return dashboard.model.getters.getGlobalFilters();
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @returns {number | undefined}
|
||||
*/
|
||||
getInitialActiveDashboard() {
|
||||
if (this.props.state && this.props.state.activeDashboardId) {
|
||||
return this.props.state.activeDashboardId;
|
||||
}
|
||||
const params = this.props.action.params || this.props.action.context.params;
|
||||
if (params && params.dashboard_id) {
|
||||
return params.dashboard_id;
|
||||
}
|
||||
const [firstSection] = this.getDashboardGroups();
|
||||
if (firstSection && firstSection.dashboards.length) {
|
||||
return firstSection.dashboards[0].id;
|
||||
}
|
||||
}
|
||||
|
||||
getDashboardGroups() {
|
||||
return this.loader.getDashboardGroups();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} dashboardId
|
||||
*/
|
||||
openDashboard(dashboardId) {
|
||||
this.state.activeDashboard = this.loader.getDashboard(dashboardId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {number} dashboardId
|
||||
* @returns {Promise<{ data: string, revisions: object[] }>}
|
||||
*/
|
||||
async _fetchDashboardData(dashboardId) {
|
||||
const [record] = await this.orm.read("spreadsheet.dashboard", [dashboardId], ["raw"]);
|
||||
return { data: record.raw, revisions: [] };
|
||||
}
|
||||
}
|
||||
SpreadsheetDashboardAction.template = "spreadsheet_dashboard.DashboardAction";
|
||||
SpreadsheetDashboardAction.components = {
|
||||
ControlPanel,
|
||||
Spreadsheet,
|
||||
FilterValue,
|
||||
DashboardMobileSearchPanel,
|
||||
MobileFigureContainer,
|
||||
};
|
||||
|
||||
registry
|
||||
.category("actions")
|
||||
.add("action_spreadsheet_dashboard", SpreadsheetDashboardAction, { force: true });
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
.o_spreadsheet_dashboard_search_panel {
|
||||
width: fit-content;
|
||||
max-width: 200px;
|
||||
align-items: center;
|
||||
|
||||
ul {
|
||||
padding-inline-start: 0px
|
||||
}
|
||||
|
||||
li {
|
||||
padding: 4px 8px 4px 12px;
|
||||
list-style-type: none;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&:hover:not(.active) {
|
||||
background-color: $o-gray-100;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.o_spreadsheet_dashboard_action {
|
||||
background-color: white;
|
||||
|
||||
.o_renderer {
|
||||
height: 100%;
|
||||
|
||||
.o-spreadsheet {
|
||||
height: 100%;
|
||||
|
||||
.o-grid {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
canvas {
|
||||
border-top: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_side_panel_filter_icon {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.dashboard-loading-status {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.o_cp_top_left {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.o_cp_top_right {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex: 4;
|
||||
row-gap: 8px;
|
||||
min-width: 0;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.o_filter_value_container {
|
||||
width: 235px;
|
||||
padding-right: 18px;
|
||||
}
|
||||
|
||||
.o-filter-value {
|
||||
min-height: 25px;
|
||||
margin-left: 8px;
|
||||
margin-right: 8px;
|
||||
min-width: 100px;
|
||||
|
||||
.o_field_many2many_tags {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.o_field_many2manytags,
|
||||
.date_filter_values {
|
||||
display: flex;
|
||||
gap: 3px;
|
||||
align-items: baseline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<templates>
|
||||
<div t-name="spreadsheet_dashboard.DashboardAction" owl="1" class="o_action o_spreadsheet_dashboard_action o_field_highlight">
|
||||
<ControlPanel display="controlPanelDisplay">
|
||||
<t t-set-slot="control-panel-top-right">
|
||||
<t t-set="status" t-value="state.activeDashboard and state.activeDashboard.status"/>
|
||||
<div t-if="status === Status.Loaded"
|
||||
class="o_filter_value_container"
|
||||
t-foreach="filters"
|
||||
t-key="activeDashboardId + '_' + filter.id"
|
||||
t-as="filter">
|
||||
<FilterValue
|
||||
filter="filter"
|
||||
model="state.activeDashboard.model"
|
||||
showTitle="true"
|
||||
/>
|
||||
</div>
|
||||
</t>
|
||||
</ControlPanel>
|
||||
<t t-set="dashboard" t-value="state.activeDashboard"/>
|
||||
<div class="o_content o_component_with_search_panel" t-att-class="{ o_mobile_dashboard: env.isSmall }">
|
||||
<!-- Dashboard selection -->
|
||||
<t t-if="env.isSmall">
|
||||
<DashboardMobileSearchPanel
|
||||
onDashboardSelected="(dashboardId) => this.openDashboard(dashboardId)"
|
||||
activeDashboard="dashboard"
|
||||
groups="getDashboardGroups()"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="o_spreadsheet_dashboard_search_panel o_search_panel flex-grow-0 border-end flex-shrink-0 pe-2 pb-5 ps-4 h-100 bg-view overflow-auto">
|
||||
<section t-foreach="getDashboardGroups()" t-as="group" t-key="group.id" class="o_search_panel_section o_search_panel_category">
|
||||
<header class="o_search_panel_section_header pt-4 pb-2 text-uppercase o_cursor_default user-select-none">
|
||||
<b t-esc="group.name"/>
|
||||
</header>
|
||||
<ul class="list-group d-block o_search_panel_field">
|
||||
<li t-foreach="group.dashboards" t-as="dashboard" t-key="dashboard.id"
|
||||
t-on-click="() => this.openDashboard(dashboard.id)"
|
||||
t-esc="dashboard.displayName"
|
||||
t-att-data-name="dashboard.displayName"
|
||||
t-att-title="dashboard.displayName"
|
||||
class="o_search_panel_category_value list-group-item cursor-pointer border-0"
|
||||
t-att-class="{'active': dashboard.id === state.activeDashboard.id}"/>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
</t>
|
||||
<!-- Main content -->
|
||||
<h3 t-if="!dashboard" class="dashboard-loading-status">No available dashboard</h3>
|
||||
<t t-else="">
|
||||
<t t-set="status" t-value="dashboard.status"/>
|
||||
<h3 t-if="status === Status.Loading" class="dashboard-loading-status">Loading...</h3>
|
||||
<div t-elif="status === Status.Error" class="dashboard-loading-status error">
|
||||
An error occured while loading the dashboard
|
||||
</div>
|
||||
<t t-else="">
|
||||
<MobileFigureContainer t-if="env.isSmall" spreadsheetModel="dashboard.model" t-key="dashboard.id"/>
|
||||
<div t-else="" class="o_renderer">
|
||||
<Spreadsheet
|
||||
model="dashboard.model"
|
||||
t-key="dashboard.id"/>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,225 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { DataSources } from "@spreadsheet/data_sources/data_sources";
|
||||
import { migrate } from "@spreadsheet/o_spreadsheet/migration";
|
||||
import spreadsheet from "@spreadsheet/o_spreadsheet/o_spreadsheet_extended";
|
||||
|
||||
const { Model } = spreadsheet;
|
||||
|
||||
/**
|
||||
* @type {{
|
||||
* NotLoaded: "NotLoaded",
|
||||
* Loading: "Loading",
|
||||
* Loaded: "Loaded",
|
||||
* Error: "Error",
|
||||
* }}
|
||||
*/
|
||||
export const Status = {
|
||||
NotLoaded: "NotLoaded",
|
||||
Loading: "Loading",
|
||||
Loaded: "Loaded",
|
||||
Error: "Error",
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef Dashboard
|
||||
* @property {number} id
|
||||
* @property {string} displayName
|
||||
* @property {string} status
|
||||
* @property {Model} [model]
|
||||
* @property {Error} [error]
|
||||
*
|
||||
* @typedef DashboardGroupData
|
||||
* @property {number} id
|
||||
* @property {string} name
|
||||
* @property {Array<number>} dashboardIds
|
||||
*
|
||||
* @typedef DashboardGroup
|
||||
* @property {number} id
|
||||
* @property {string} name
|
||||
* @property {Array<Dashboard>} dashboards
|
||||
*
|
||||
* @typedef {(dashboardId: number) => Promise<{ data: string, revisions: object[] }>} FetchDashboardData
|
||||
*
|
||||
* @typedef {import("@web/env").OdooEnv} OdooEnv
|
||||
*
|
||||
* @typedef {import("@web/core/orm_service").ORM} ORM
|
||||
*/
|
||||
|
||||
export class DashboardLoader {
|
||||
/**
|
||||
* @param {OdooEnv} env
|
||||
* @param {ORM} orm
|
||||
* @param {FetchDashboardData} fetchDashboardData
|
||||
*/
|
||||
constructor(env, orm, fetchDashboardData) {
|
||||
/** @private */
|
||||
this.env = env;
|
||||
/** @private */
|
||||
this.orm = orm;
|
||||
/** @private @type {Array<DashboardGroupData>} */
|
||||
this.groups = [];
|
||||
/** @private @type {Object<number, Dashboard>} */
|
||||
this.dashboards = {};
|
||||
/** @private */
|
||||
this.fetchDashboardData = fetchDashboardData;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array<DashboardGroupData>} groups
|
||||
* @param {Object<number, Dashboard>} dashboards
|
||||
*/
|
||||
restoreFromState(groups, dashboards) {
|
||||
this.groups = groups;
|
||||
this.dashboards = dashboards;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return data needed to restore a dashboard loader
|
||||
*/
|
||||
getState() {
|
||||
return {
|
||||
groups: this.groups,
|
||||
dashboards: this.dashboards,
|
||||
};
|
||||
}
|
||||
|
||||
async load() {
|
||||
const groups = await this._fetchGroups();
|
||||
this.groups = groups
|
||||
.filter((group) => group.dashboard_ids.length)
|
||||
.map((group) => ({
|
||||
id: group.id,
|
||||
name: group.name,
|
||||
dashboardIds: group.dashboard_ids,
|
||||
}));
|
||||
const dashboards = await this._fetchDashboardNames(this.groups);
|
||||
for (const dashboard of dashboards) {
|
||||
this.dashboards[dashboard.id] = {
|
||||
id: dashboard.id,
|
||||
displayName: dashboard.name,
|
||||
status: Status.NotLoaded,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} dashboardId
|
||||
* @returns {Dashboard}
|
||||
*/
|
||||
getDashboard(dashboardId) {
|
||||
const dashboard = this._getDashboard(dashboardId);
|
||||
if (dashboard.status === Status.NotLoaded) {
|
||||
dashboard.promise = this._loadDashboardData(dashboardId);
|
||||
}
|
||||
return dashboard;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Array<DashboardGroup>}
|
||||
*/
|
||||
getDashboardGroups() {
|
||||
return this.groups.map((section) => ({
|
||||
id: section.id,
|
||||
name: section.name,
|
||||
dashboards: section.dashboardIds.map((dashboardId) => ({
|
||||
id: dashboardId,
|
||||
displayName: this._getDashboard(dashboardId).displayName,
|
||||
status: this._getDashboard(dashboardId).status,
|
||||
})),
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @returns {Promise<{id: number, name: string, dashboard_ids: number[]}[]>}
|
||||
*/
|
||||
_fetchGroups() {
|
||||
return this.orm.searchRead(
|
||||
"spreadsheet.dashboard.group",
|
||||
[["dashboard_ids", "!=", false]],
|
||||
["id", "name", "dashboard_ids"]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {Array<DashboardGroupData>} groups
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_fetchDashboardNames(groups) {
|
||||
return this.orm.read(
|
||||
"spreadsheet.dashboard",
|
||||
groups.map((group) => group.dashboardIds).flat(),
|
||||
["name"]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {number} id
|
||||
* @returns {Dashboard}
|
||||
*/
|
||||
_getDashboard(id) {
|
||||
if (!this.dashboards[id]) {
|
||||
this.dashboards[id] = { status: Status.NotLoaded, id, displayName: "" };
|
||||
}
|
||||
return this.dashboards[id];
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {number} dashboardId
|
||||
*/
|
||||
async _loadDashboardData(dashboardId) {
|
||||
const dashboard = this._getDashboard(dashboardId);
|
||||
dashboard.status = Status.Loading;
|
||||
try {
|
||||
const { data, revisions } = await this.fetchDashboardData(dashboardId);
|
||||
dashboard.model = this._createSpreadsheetModel(data, revisions);
|
||||
dashboard.status = Status.Loaded;
|
||||
} catch (error) {
|
||||
dashboard.error = error;
|
||||
dashboard.status = Status.Error;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate the first sheet of a model
|
||||
*
|
||||
* @param {Model} model
|
||||
*/
|
||||
_activateFirstSheet(model) {
|
||||
const sheetId = model.getters.getActiveSheetId();
|
||||
const firstSheetId = model.getters.getSheetIds()[0];
|
||||
if (firstSheetId !== sheetId) {
|
||||
model.dispatch("ACTIVATE_SHEET", {
|
||||
sheetIdFrom: sheetId,
|
||||
sheetIdTo: firstSheetId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {string} data
|
||||
* @param {object[]} revisions
|
||||
* @returns {Model}
|
||||
*/
|
||||
_createSpreadsheetModel(data, revisions = []) {
|
||||
const dataSources = new DataSources(this.orm);
|
||||
const model = new Model(
|
||||
migrate(JSON.parse(data)),
|
||||
{
|
||||
evalContext: { env: this.env, orm: this.orm },
|
||||
mode: "dashboard",
|
||||
dataSources,
|
||||
},
|
||||
revisions
|
||||
);
|
||||
this._activateFirstSheet(model);
|
||||
dataSources.addEventListener("data-source-updated", () => model.dispatch("EVALUATE_CELLS"));
|
||||
return model;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import spreadsheet from "@spreadsheet/o_spreadsheet/o_spreadsheet_extended";
|
||||
|
||||
const { Component, useSubEnv } = owl;
|
||||
const { registries } = spreadsheet;
|
||||
const { figureRegistry } = registries;
|
||||
|
||||
export class MobileFigureContainer extends Component {
|
||||
setup() {
|
||||
useSubEnv({
|
||||
model: this.props.spreadsheetModel,
|
||||
isDashboard: () => this.props.spreadsheetModel.getters.isDashboard(),
|
||||
});
|
||||
}
|
||||
|
||||
get figures() {
|
||||
const sheetId = this.props.spreadsheetModel.getters.getActiveSheetId();
|
||||
return this.props.spreadsheetModel.getters
|
||||
.getFigures(sheetId)
|
||||
.sort((f1, f2) => (this.isBefore(f1, f2) ? -1 : 1))
|
||||
.map((figure) => ({
|
||||
...figure,
|
||||
width: window.innerWidth,
|
||||
}));
|
||||
}
|
||||
|
||||
getFigureComponent(figure) {
|
||||
return figureRegistry.get(figure.tag).Component;
|
||||
}
|
||||
|
||||
isBefore(f1, f2) {
|
||||
// TODO be smarter
|
||||
return f1.x < f2.x ? f1.y < f2.y : f1.y < f2.y;
|
||||
}
|
||||
}
|
||||
|
||||
MobileFigureContainer.template = "documents_spreadsheet.MobileFigureContainer";
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<templates>
|
||||
<t t-name="documents_spreadsheet.MobileFigureContainer" owl="1">
|
||||
<t t-if="!figures.length">
|
||||
Only chart figures are displayed in small screens but this dashboard doesn't contain any
|
||||
</t>
|
||||
<div
|
||||
t-foreach="figures" t-as="figure"
|
||||
t-key="figure.id"
|
||||
t-attf-style="min-height: #{figure.height}px;"
|
||||
>
|
||||
<t t-component="getFigureComponent(figure)" figure="figure"/>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
|
||||
const { Component, useState } = owl;
|
||||
|
||||
export class DashboardMobileSearchPanel extends Component {
|
||||
setup() {
|
||||
this.state = useState({ isOpen: false });
|
||||
}
|
||||
|
||||
get searchBarText() {
|
||||
return this.props.activeDashboard
|
||||
? this.props.activeDashboard.displayName
|
||||
: _t("Choose a dashboard....");
|
||||
}
|
||||
|
||||
onDashboardSelected(dashboardId) {
|
||||
this.props.onDashboardSelected(dashboardId);
|
||||
this.state.isOpen = false;
|
||||
}
|
||||
|
||||
openDashboardSelection() {
|
||||
const dashboards = this.props.groups.map((group) => group.dashboards).flat();
|
||||
if (dashboards.length > 1) {
|
||||
this.state.isOpen = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DashboardMobileSearchPanel.template = "documents_spreadsheet.DashboardMobileSearchPanel";
|
||||
DashboardMobileSearchPanel.props = {
|
||||
/**
|
||||
* (dashboardId: number) => void
|
||||
*/
|
||||
onDashboardSelected: Function,
|
||||
groups: Object,
|
||||
activeDashboard: {
|
||||
type: Object,
|
||||
optional: true,
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<templates>
|
||||
<div t-name="documents_spreadsheet.DashboardMobileSearchPanel" class="o_search_panel o_search_panel_summary btn w-100 overflow-visible" owl="1">
|
||||
<t t-if="state.isOpen">
|
||||
<t t-portal="'body'">
|
||||
<div class="o_spreadsheet_dashboard_search_panel o_search_panel o_searchview o_mobile_search">
|
||||
<div class="o_mobile_search_header">
|
||||
<button type="button" class="o_mobile_search_button btn" t-on-click="() => this.state.isOpen = false">
|
||||
<i class="fa fa-arrow-left" />
|
||||
<strong class="ml8">BACK</strong>
|
||||
</button>
|
||||
</div>
|
||||
<div class="o_mobile_search_content">
|
||||
<div class="o_search_panel flex-grow-0 flex-shrink-0 border-end pe-2 pb-5 ps-4 h-100 bg-view overflow-auto">
|
||||
<section t-foreach="props.groups" t-as="group" t-key="group.id" class="o_search_panel_section o_search_panel_category">
|
||||
<header class="o_search_panel_section_header pt-4 pb-2 text-uppercase cursor-default">
|
||||
<b t-esc="group.name"/>
|
||||
</header>
|
||||
<ul class="list-group d-block o_search_panel_field">
|
||||
<li t-foreach="group.dashboards" t-as="dashboard" t-key="dashboard.id" t-on-click="() => this.onDashboardSelected(dashboard.id)" class="o_search_panel_category_value list-group-item py-1 o_cursor_pointer border-0 ps-0 pe-2">
|
||||
<header class="list-group-item list-group-item-action d-flex align-items-center p-0 border-0" t-att-class="{'active text-900 fw-bold': dashboard.id === props.activeDashboard.id}">
|
||||
<div class="o_search_panel_label d-flex align-items-center overflow-hidden w-100 o_cursor_pointer mb-0">
|
||||
<!-- empty button to mimick the standard search panel -->
|
||||
<button class="o_toggle_fold btn p-0 flex-shrink-0 text-center"/>
|
||||
<span t-esc="dashboard.displayName" class="o_search_panel_label_title text-truncate"/>
|
||||
</div>
|
||||
|
||||
</header>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
<div t-elif="props.groups.length" t-on-click="openDashboardSelection" class="d-flex align-items-center">
|
||||
<i class="fa fa-fw fa-filter"/>
|
||||
<div t-esc="searchBarText" class="o_search_panel_current_selection text-truncate ms-2 me-auto"/>
|
||||
</div>
|
||||
</div>
|
||||
</templates>
|
||||
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import spreadsheet from "@spreadsheet/o_spreadsheet/o_spreadsheet_extended";
|
||||
|
||||
export default class DashboardLinkPlugin extends spreadsheet.UIPlugin {
|
||||
constructor(getters, state, dispatch, config, selection) {
|
||||
super(...arguments);
|
||||
this.env = config.evalContext.env;
|
||||
this.selection.observe(this, {
|
||||
handleEvent: this.handleEvent.bind(this),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
handleEvent(event) {
|
||||
if (!this.getters.isDashboard()) {
|
||||
return;
|
||||
}
|
||||
switch (event.type) {
|
||||
case "ZonesSelected": {
|
||||
const sheetId = this.getters.getActiveSheetId();
|
||||
const { col, row } = event.anchor.cell;
|
||||
const cell = this.getters.getCell(sheetId, col, row);
|
||||
if (cell !== undefined && cell.isLink()) {
|
||||
cell.action(this.env);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import spreadsheet from "@spreadsheet/o_spreadsheet/o_spreadsheet_extended";
|
||||
import DashboardLinkPlugin from "./dashboard_link_plugin";
|
||||
|
||||
const { uiPluginRegistry } = spreadsheet.registries;
|
||||
|
||||
uiPluginRegistry.add("odooDashboardClickLink", DashboardLinkPlugin);
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { SEE_RECORD_LIST, SEE_RECORD_LIST_VISIBLE } from "@spreadsheet/list/list_actions";
|
||||
import spreadsheet from "@spreadsheet/o_spreadsheet/o_spreadsheet_extended";
|
||||
|
||||
const { clickableCellRegistry } = spreadsheet.registries;
|
||||
|
||||
clickableCellRegistry.add("list", {
|
||||
condition: SEE_RECORD_LIST_VISIBLE,
|
||||
action: SEE_RECORD_LIST,
|
||||
sequence: 10,
|
||||
});
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import spreadsheet from "@spreadsheet/o_spreadsheet/o_spreadsheet_extended";
|
||||
import { SEE_RECORDS_PIVOT, SEE_RECORDS_PIVOT_VISIBLE } from "@spreadsheet/pivot/pivot_actions";
|
||||
import { getFirstPivotFunction } from "@spreadsheet/pivot/pivot_helpers";
|
||||
|
||||
const { clickableCellRegistry } = spreadsheet.registries;
|
||||
|
||||
clickableCellRegistry.add("pivot", {
|
||||
condition: SEE_RECORDS_PIVOT_VISIBLE,
|
||||
action: SEE_RECORDS_PIVOT,
|
||||
sequence: 3,
|
||||
});
|
||||
|
||||
clickableCellRegistry.add("pivot_set_filter_matching", {
|
||||
condition: (cell, env) => {
|
||||
return (
|
||||
SEE_RECORDS_PIVOT_VISIBLE(cell, env) &&
|
||||
getFirstPivotFunction(cell.content).functionName === "ODOO.PIVOT.HEADER" &&
|
||||
env.model.getters.getFiltersMatchingPivot(cell.content).length > 0
|
||||
);
|
||||
},
|
||||
action: (cell, env) => {
|
||||
const filters = env.model.getters.getFiltersMatchingPivot(cell.content);
|
||||
env.model.dispatch("SET_MANY_GLOBAL_FILTER_VALUE", { filters });
|
||||
},
|
||||
sequence: 2,
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue