Skip to content

Commit b2183d0

Browse files
committed
[IMP] dashboard: add button to open chart in full screen
This commits adds the possibility to open a chart in full screen mode. This only exists in dashboard. closes #6363 Task: 4787283 Signed-off-by: Lucas Lefèvre (lul) <lul@odoo.com>
1 parent e354859 commit b2183d0

16 files changed

+314
-19
lines changed

src/components/figures/chart/chartJs/chartjs.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { waterfallLinesPlugin } from "./chartjs_waterfall_plugin";
2020

2121
interface Props {
2222
figureUI: FigureUI;
23+
isFullScreen?: boolean;
2324
}
2425

2526
css/* scss */ `
@@ -67,6 +68,7 @@ export class ChartJsComponent extends Component<Props, SpreadsheetChildEnv> {
6768
static template = "o-spreadsheet-ChartJsComponent";
6869
static props = {
6970
figureUI: Object,
71+
isFullScreen: { type: Boolean, optional: true },
7072
};
7173

7274
private canvas = useRef("graphContainer");
@@ -123,9 +125,9 @@ export class ChartJsComponent extends Component<Props, SpreadsheetChildEnv> {
123125
private createChart(chartData: ChartConfiguration<any>) {
124126
if (this.env.model.getters.isDashboard() && this.animationStore) {
125127
const chartType = this.env.model.getters.getChart(this.props.figureUI.id)?.type;
126-
if (chartType && this.animationStore.animationPlayed[this.props.figureUI.id] !== chartType) {
128+
if (chartType && this.animationStore.animationPlayed[this.animationFigureId] !== chartType) {
127129
chartData = this.enableAnimationInChartData(chartData);
128-
this.animationStore.disableAnimationForChart(this.props.figureUI.id, chartType);
130+
this.animationStore.disableAnimationForChart(this.animationFigureId, chartType);
129131
}
130132
}
131133

@@ -139,7 +141,7 @@ export class ChartJsComponent extends Component<Props, SpreadsheetChildEnv> {
139141
const chartType = this.env.model.getters.getChart(this.props.figureUI.id)?.type;
140142
if (chartType && this.hasChartDataChanged() && this.animationStore) {
141143
chartData = this.enableAnimationInChartData(chartData);
142-
this.animationStore.disableAnimationForChart(this.props.figureUI.id, chartType);
144+
this.animationStore.disableAnimationForChart(this.animationFigureId, chartType);
143145
}
144146
}
145147

@@ -168,4 +170,10 @@ export class ChartJsComponent extends Component<Props, SpreadsheetChildEnv> {
168170
options: { ...chartData.options, animation: { animateRotate: true } },
169171
};
170172
}
173+
174+
get animationFigureId() {
175+
return this.props.isFullScreen
176+
? this.props.figureUI.id + "-fullscreen"
177+
: this.props.figureUI.id;
178+
}
171179
}

src/components/figures/chart/chartJs/chartjs_animation_store.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,17 @@ import { SpreadsheetStore } from "../../../../stores/spreadsheet_store";
22
import { ChartType, UID } from "../../../../types";
33

44
export class ChartAnimationStore extends SpreadsheetStore {
5-
mutators = ["disableAnimationForChart"] as const;
5+
mutators = ["disableAnimationForChart", "enableAnimationForChart"] as const;
66

77
animationPlayed = {};
88

99
disableAnimationForChart(chartId: UID, chartType: ChartType) {
1010
this.animationPlayed[chartId] = chartType;
1111
return "noStateChange";
1212
}
13+
14+
enableAnimationForChart(chartId: UID) {
15+
this.animationPlayed[chartId] = undefined;
16+
return "noStateChange";
17+
}
1318
}

src/components/figures/chart/chart_dashboard_menu/chart_dashboard_menu.ts

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,39 @@
11
import { Component, onWillUpdateProps, useState } from "@odoo/owl";
22
import { getChartMenuActions } from "../../../../actions/figure_menu_actions";
33
import { BACKGROUND_CHART_COLOR } from "../../../../constants";
4+
import { isDefined } from "../../../../helpers";
45
import { chartRegistry, chartSubtypeRegistry } from "../../../../registries/chart_types";
6+
import { Store, useStore } from "../../../../store_engine";
7+
import { _t } from "../../../../translation";
58
import { ChartDefinition, ChartType, FigureUI, SpreadsheetChildEnv } from "../../../../types";
9+
import { FullScreenChartStore } from "../../../full_screen_chart/full_screen_chart_store";
610
import { Menu, MenuState } from "../../../menu/menu";
711

812
interface Props {
913
figureUI: FigureUI;
1014
}
1115

16+
interface MenuItem {
17+
id: string;
18+
label: string;
19+
iconClass: string;
20+
onClick: () => void;
21+
isSelected?: boolean;
22+
}
23+
1224
export class ChartDashboardMenu extends Component<Props, SpreadsheetChildEnv> {
1325
static template = "spreadsheet.ChartDashboardMenu";
1426
static components = { Menu };
1527
static props = { figureUI: Object };
1628

1729
private originalChartDefinition!: ChartDefinition;
30+
private fullScreenFigureStore!: Store<FullScreenChartStore>;
1831

1932
private menuState: MenuState = useState({ isOpen: false, anchorRect: null, menuItems: [] });
2033

2134
setup() {
2235
super.setup();
36+
this.fullScreenFigureStore = useStore(FullScreenChartStore);
2337
this.originalChartDefinition = this.env.model.getters.getChartDefinition(
2438
this.props.figureUI.id
2539
);
@@ -30,7 +44,11 @@ export class ChartDashboardMenu extends Component<Props, SpreadsheetChildEnv> {
3044
});
3145
}
3246

33-
getAvailableTypes() {
47+
getMenuItems(): MenuItem[] {
48+
return [this.fullScreenMenuItem, ...this.changeChartTypeMenuItems].filter(isDefined);
49+
}
50+
51+
get changeChartTypeMenuItems(): MenuItem[] {
3452
const definition = this.env.model.getters.getChartDefinition(this.props.figureUI.id);
3553
if (!["line", "bar", "pie"].includes(definition.type)) {
3654
return [];
@@ -39,8 +57,11 @@ export class ChartDashboardMenu extends Component<Props, SpreadsheetChildEnv> {
3957
return ["column", "line", "pie"].map((type) => {
4058
const item = chartSubtypeRegistry.get(type);
4159
return {
42-
...item,
43-
icon: this.getIconClasses(item.chartType),
60+
id: item.chartType,
61+
label: item.displayName,
62+
onClick: () => this.onTypeChange(item.chartType),
63+
isSelected: item.chartType === this.selectedChartType,
64+
iconClass: this.getIconClasses(item.chartType),
4465
};
4566
});
4667
}
@@ -100,4 +121,30 @@ export class ChartDashboardMenu extends Component<Props, SpreadsheetChildEnv> {
100121
this.menuState.anchorRect = { x: ev.clientX, y: ev.clientY, width: 0, height: 0 };
101122
this.menuState.menuItems = getChartMenuActions(this.props.figureUI.id, () => {}, this.env);
102123
}
124+
125+
get fullScreenMenuItem(): MenuItem | undefined {
126+
const definition = this.env.model.getters.getChartDefinition(this.props.figureUI.id);
127+
if (definition.type === "scorecard") {
128+
return undefined;
129+
}
130+
131+
if (this.props.figureUI.id === this.fullScreenFigureStore.fullScreenFigure?.id) {
132+
return {
133+
id: "fullScreenChart",
134+
label: _t("Exit Full Screen"),
135+
iconClass: "fa fa-compress",
136+
onClick: () => {
137+
this.fullScreenFigureStore.toggleFullScreenChart(this.props.figureUI.id);
138+
},
139+
};
140+
}
141+
return {
142+
id: "fullScreenChart",
143+
label: _t("Full Screen"),
144+
iconClass: "fa fa-expand",
145+
onClick: () => {
146+
this.fullScreenFigureStore.toggleFullScreenChart(this.props.figureUI.id);
147+
},
148+
};
149+
}
103150
}

src/components/figures/chart/chart_dashboard_menu/chart_dashboard_menu.xml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22
<t t-name="spreadsheet.ChartDashboardMenu">
33
<div class="o-dashboard-chart-select position-absolute top-0 end-0" t-on-click.stop="">
44
<div class="d-flex flex-row px-1" t-att-style="backgroundColor">
5-
<t t-foreach="getAvailableTypes()" t-as="type" t-key="type.chartSubtype">
5+
<t t-foreach="getMenuItems()" t-as="item" t-key="item.id">
66
<button
7-
t-attf-class=" {{type.icon}} {{type.chartType === selectedChartType ? 'active' : ''}}"
7+
t-attf-class=" {{item.iconClass}} {{item.isSelected ? 'active' : ''}}"
88
class="o-chart-dashboard-item btn mt-1 me-1 p-1 "
9-
t-att-title="type.displayName"
10-
t-on-click="() => this.onTypeChange(type.chartSubtype)"
11-
t-att-data-id="type.chartSubtype"
9+
t-att-title="item.label"
10+
t-on-click="item.onClick"
11+
t-att-data-id="item.id"
1212
/>
1313
</t>
1414
<button

src/components/figures/chart/gauge/gauge_chart_component.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,16 @@ import { GaugeChartRuntime } from "../../../../types/chart";
55

66
interface Props {
77
figureUI: FigureUI;
8+
isFullScreen?: boolean;
89
}
910

1011
export class GaugeChartComponent extends Component<Props, SpreadsheetChildEnv> {
1112
static template = "o-spreadsheet-GaugeChartComponent";
13+
static props = {
14+
figureUI: Object,
15+
isFullScreen: { type: Boolean, optional: true },
16+
};
17+
1218
private canvas = useRef("chartContainer");
1319

1420
get runtime(): GaugeChartRuntime {
@@ -26,7 +32,3 @@ export class GaugeChartComponent extends Component<Props, SpreadsheetChildEnv> {
2632
);
2733
}
2834
}
29-
30-
GaugeChartComponent.props = {
31-
figureUI: Object,
32-
};
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
.o-spreadsheet {
2+
.o-fullscreen-chart-overlay {
3+
z-index: 34; /* TODO: use css variables once ComponentsImportance is available in the scss. */
4+
background-color: rgba(0, 0, 0, 0.4);
5+
padding: 60px;
6+
7+
.o-figure:not(:hover) .o-dashboard-chart-select {
8+
display: block !important;
9+
}
10+
}
11+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { Component, onWillUpdateProps, useEffect, useRef } from "@odoo/owl";
2+
import { chartComponentRegistry } from "../../registries/chart_types";
3+
import { figureRegistry } from "../../registries/figures_registry";
4+
import { Store, useStore } from "../../store_engine";
5+
import { SpreadsheetChildEnv } from "../../types";
6+
import { ChartDashboardMenu } from "../figures/chart/chart_dashboard_menu/chart_dashboard_menu";
7+
import { ChartAnimationStore } from "../figures/chart/chartJs/chartjs_animation_store";
8+
import { useSpreadsheetRect } from "../helpers/position_hook";
9+
import { FullScreenChartStore } from "./full_screen_chart_store";
10+
11+
export class FullScreenChart extends Component<{}, SpreadsheetChildEnv> {
12+
static template = "o-spreadsheet-FullScreenChart";
13+
static props = {};
14+
static components = { ChartDashboardMenu };
15+
16+
private fullScreenChartStore!: Store<FullScreenChartStore>;
17+
private ref = useRef("fullScreenChart");
18+
19+
spreadsheetRect = useSpreadsheetRect();
20+
21+
figureRegistry = figureRegistry;
22+
23+
setup() {
24+
this.fullScreenChartStore = useStore(FullScreenChartStore);
25+
26+
const animationStore = useStore(ChartAnimationStore);
27+
let lastFigureId: string | undefined = undefined;
28+
onWillUpdateProps(() => {
29+
if (lastFigureId !== this.figureUI?.id) {
30+
animationStore.enableAnimationForChart(this.figureUI?.id + "-fullscreen");
31+
}
32+
lastFigureId = this.figureUI?.id;
33+
});
34+
35+
useEffect(
36+
(el) => el?.focus(),
37+
() => [this.ref.el]
38+
);
39+
}
40+
41+
get figureUI() {
42+
return this.fullScreenChartStore.fullScreenFigure;
43+
}
44+
45+
exitFullScreen() {
46+
if (this.figureUI) {
47+
this.fullScreenChartStore.toggleFullScreenChart(this.figureUI.id);
48+
}
49+
}
50+
51+
onKeyDown(ev: KeyboardEvent) {
52+
if (ev.key === "Escape") {
53+
this.exitFullScreen();
54+
}
55+
}
56+
57+
get chartComponent(): (new (...args: any) => Component) | undefined {
58+
if (!this.figureUI) return undefined;
59+
const type = this.env.model.getters.getChartType(this.figureUI.id);
60+
const component = chartComponentRegistry.get(type);
61+
if (!component) {
62+
throw new Error(`Component is not defined for type ${type}`);
63+
}
64+
return component;
65+
}
66+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<templates>
2+
<t t-name="o-spreadsheet-FullScreenChart">
3+
<div
4+
class="position-absolute o-fullscreen-chart-overlay w-100 h-100 d-flex"
5+
t-if="figureUI"
6+
t-on-click="exitFullScreen">
7+
<button
8+
class="o-exit top-0 end-0 position-absolute o-button primary m-1"
9+
t-on-click="exitFullScreen">
10+
Exit fullscreen
11+
</button>
12+
<div class="flex-fill">
13+
<div
14+
class="o-fullscreen-chart o-figure position-relative"
15+
tabindex="1"
16+
t-ref="fullScreenChart"
17+
t-on-click.stop=""
18+
t-on-keydown="(ev) => this.onKeyDown(ev)">
19+
<t
20+
t-component="chartComponent"
21+
figureUI="this.figureUI"
22+
isFullScreen="true"
23+
t-key="this.figureUI.id"
24+
/>
25+
<div class="position-absolute top-0 end-0">
26+
<ChartDashboardMenu figureUI="figureUI"/>
27+
</div>
28+
</div>
29+
</div>
30+
</div>
31+
</t>
32+
</templates>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { SpreadsheetStore } from "../../stores";
2+
import { FigureUI, UID } from "../../types";
3+
4+
export class FullScreenChartStore extends SpreadsheetStore {
5+
mutators = ["toggleFullScreenChart"] as const;
6+
7+
fullScreenFigure: FigureUI | undefined = undefined;
8+
9+
toggleFullScreenChart(figureId: string) {
10+
if (this.fullScreenFigure?.id === figureId) {
11+
this.fullScreenFigure = undefined;
12+
} else {
13+
this.makeFullScreen(figureId);
14+
}
15+
}
16+
17+
private makeFullScreen(figureId: UID) {
18+
const sheetId = this.getters.getActiveSheetId();
19+
const figure = this.getters.getFigure(sheetId, figureId);
20+
if (figure) {
21+
this.fullScreenFigure = { ...figure, x: 0, y: 0, width: 0, height: 0 };
22+
}
23+
}
24+
}

src/components/helpers/position_hook.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ export function usePopoverContainer(): Rect {
3939
function updateRect() {
4040
const env = component.env;
4141
const newRect =
42-
"getPopoverContainerRect" in env ? env.getPopoverContainerRect() : spreadsheetRect;
42+
"getPopoverContainerRect" in env && env.getPopoverContainerRect
43+
? env.getPopoverContainerRect()
44+
: spreadsheetRect;
4345
container.x = newRect.x;
4446
container.y = newRect.y;
4547
container.width = newRect.width;

0 commit comments

Comments
 (0)