Skip to content

Commit 86d242f

Browse files
authored
Merge pull request #1541 from visualize-admin/feat/dashboard-action-menu
Dashboard action menu
2 parents 072a53d + 4c43c46 commit 86d242f

26 files changed

+660
-335
lines changed

CHANGELOG.md

+11
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
You can also check the [release page](https://github.com/visualize-admin/visualization-tool/releases)
99

10+
# Unreleased
11+
12+
- Features
13+
- Add the "Free canvas" layout, allowing users to freely resize and move charts for dashboards
14+
- Ability to start a chart from another dataset than the current one
15+
16+
# [4.5.1] - 2024-05-21
17+
18+
- Fixes
19+
- If there is an error on a chart contained in a dashboard, the layout is not be broken
20+
1021
# [4.5.0] - 2024-05-21
1122

1223
- Features

app/components/chart-panel-layout-grid.tsx

+8-9
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
import clsx from "clsx";
21
import { fold } from "fp-ts/lib/Either";
32
import { pipe } from "fp-ts/lib/function";
43
import { useState } from "react";
54
import { Layouts } from "react-grid-layout";
65

76
import { ChartPanelLayoutTypeProps } from "@/components/chart-panel";
87
import ChartGridLayout from "@/components/react-grid";
9-
import { ReactGridLayoutsType, isLayouting } from "@/configurator";
8+
import { isLayouting, ReactGridLayoutsType } from "@/configurator";
109
import { useConfiguratorState } from "@/src";
1110
import { assert } from "@/utils/assert";
1211

@@ -30,13 +29,13 @@ const decodeLayouts = (layouts: Layouts) => {
3029
);
3130
};
3231

33-
const ChartPanelLayoutGrid = (props: ChartPanelLayoutTypeProps) => {
32+
const ChartPanelLayoutCanvas = (props: ChartPanelLayoutTypeProps) => {
3433
const { chartConfigs } = props;
3534
const [config, dispatch] = useConfiguratorState(isLayouting);
3635
const [layouts, setLayouts] = useState<Layouts>(() => {
3736
assert(
38-
config.layout.type === "dashboard" && config.layout.layout === "tiles",
39-
"ChartPanelLayoutGrid should be rendered only for dashboard layout with tiles"
37+
config.layout.type === "dashboard" && config.layout.layout === "canvas",
38+
"ChartPanelLayoutGrid should be rendered only for dashboard layout with canvas"
4039
);
4140

4241
return config.layout.layouts;
@@ -45,8 +44,8 @@ const ChartPanelLayoutGrid = (props: ChartPanelLayoutTypeProps) => {
4544
const handleChangeLayouts = (layouts: Layouts) => {
4645
const layout = config.layout;
4746
assert(
48-
layout.type === "dashboard" && layout.layout === "tiles",
49-
"ChartPanelLayoutGrid should be rendered only for dashboard layout with tiles"
47+
layout.type === "dashboard" && layout.layout === "canvas",
48+
"ChartPanelLayoutGrid should be rendered only for dashboard layout with canvas"
5049
);
5150

5251
const parsedLayouts = decodeLayouts(layouts);
@@ -66,7 +65,7 @@ const ChartPanelLayoutGrid = (props: ChartPanelLayoutTypeProps) => {
6665

6766
return (
6867
<ChartGridLayout
69-
className={clsx("layout", chartPanelLayoutGridClasses.root)}
68+
className={chartPanelLayoutGridClasses.root}
7069
layouts={layouts}
7170
resize
7271
draggableHandle={`.${chartPanelLayoutGridClasses.dragHandle}`}
@@ -77,4 +76,4 @@ const ChartPanelLayoutGrid = (props: ChartPanelLayoutTypeProps) => {
7776
);
7877
};
7978

80-
export default ChartPanelLayoutGrid;
79+
export default ChartPanelLayoutCanvas;

app/components/chart-panel.tsx

+17-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { Box, BoxProps, Theme } from "@mui/material";
22
import { makeStyles } from "@mui/styles";
33
import clsx from "clsx";
4-
import React, { HTMLProps, forwardRef } from "react";
4+
import React, { forwardRef, HTMLProps } from "react";
55

6-
import ChartPanelLayoutGrid from "@/components/chart-panel-layout-grid";
6+
import ChartPanelLayoutCanvas, {
7+
chartPanelLayoutGridClasses,
8+
} from "@/components/chart-panel-layout-grid";
79
import { ChartPanelLayoutTall } from "@/components/chart-panel-layout-tall";
810
import { ChartPanelLayoutVertical } from "@/components/chart-panel-layout-vertical";
911
import { ChartSelectionTabs } from "@/components/chart-selection-tabs";
@@ -15,6 +17,17 @@ const useStyles = makeStyles((theme: Theme) => ({
1517
flexDirection: "column",
1618
gap: theme.spacing(4),
1719
},
20+
chartWrapper: {
21+
[`.${chartPanelLayoutGridClasses.root} &`]: {
22+
transition: theme.transitions.create(["box-shadow"], {
23+
duration: theme.transitions.duration.shortest,
24+
}),
25+
},
26+
[`.${chartPanelLayoutGridClasses.root} &:has(.${chartPanelLayoutGridClasses.dragHandle}:hover)`]:
27+
{
28+
boxShadow: theme.shadows[6],
29+
},
30+
},
1831
chartWrapperInner: {
1932
display: "contents",
2033
overflow: "hidden",
@@ -33,7 +46,7 @@ export const ChartWrapper = forwardRef<HTMLDivElement, ChartWrapperProps>(
3346
const { children, editing, layoutType, ...rest } = props;
3447
const classes = useStyles();
3548
return (
36-
<Box ref={ref} {...rest}>
49+
<Box ref={ref} {...rest} className={classes.chartWrapper}>
3750
{(editing || layoutType === "tab") && <ChartSelectionTabs />}
3851
<Box
3952
className={classes.chartWrapperInner}
@@ -64,7 +77,7 @@ const Wrappers: Record<
6477
> = {
6578
vertical: ChartPanelLayoutVertical,
6679
tall: ChartPanelLayoutTall,
67-
tiles: ChartPanelLayoutGrid,
80+
canvas: ChartPanelLayoutCanvas,
6881
};
6982

7083
export const ChartPanelLayout = (props: ChartPanelLayoutProps) => {

app/components/chart-preview.tsx

+109-38
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import {
66
useDraggable,
77
useDroppable,
88
} from "@dnd-kit/core";
9-
import { Trans } from "@lingui/macro";
10-
import { Box } from "@mui/material";
9+
import { t, Trans } from "@lingui/macro";
10+
import { Box, IconButton, useEventCallback } from "@mui/material";
1111
import Head from "next/head";
1212
import React, {
1313
forwardRef,
@@ -20,6 +20,7 @@ import React, {
2020
import { DataSetTable } from "@/browse/datatable";
2121
import { ChartDataFilters } from "@/charts/shared/chart-data-filters";
2222
import { LoadingStateProvider } from "@/charts/shared/chart-loading-state";
23+
import { ArrowMenu } from "@/components/arrow-menu";
2324
import { ChartErrorBoundary } from "@/components/chart-error-boundary";
2425
import { ChartFootnotes } from "@/components/chart-footnotes";
2526
import {
@@ -35,10 +36,11 @@ import {
3536
import { useChartStyles } from "@/components/chart-utils";
3637
import { ChartWithFilters } from "@/components/chart-with-filters";
3738
import DebugPanel from "@/components/debug-panel";
38-
import { DragHandle } from "@/components/drag-handle";
39+
import { DragHandle, DragHandleProps } from "@/components/drag-handle";
3940
import Flex from "@/components/flex";
4041
import { Checkbox } from "@/components/form";
4142
import { HintYellow } from "@/components/hint";
43+
import { MenuActionItem } from "@/components/menu-action-item";
4244
import { MetadataPanel } from "@/components/metadata-panel";
4345
import { BANNER_MARGIN_TOP } from "@/components/presence";
4446
import {
@@ -56,6 +58,7 @@ import {
5658
useDataCubesMetadataQuery,
5759
} from "@/graphql/hooks";
5860
import { DataCubePublicationStatus } from "@/graphql/resolver-types";
61+
import SvgIcMore from "@/icons/components/IcMore";
5962
import { useLocale } from "@/locales/use-locale";
6063
import { InteractiveFiltersChartProvider } from "@/stores/interactive-filters";
6164
import { useTransitionStore } from "@/stores/transition";
@@ -109,7 +112,7 @@ const DashboardPreview = (props: DashboardPreviewProps) => {
109112
const [over, setOver] = useState<Over | null>(null);
110113
const renderChart = useCallback(
111114
(chartConfig: ChartConfig) => {
112-
return layoutType === "tiles" ? (
115+
return layoutType === "canvas" ? (
113116
<ReactGridChartPreview
114117
key={chartConfig.key}
115118
chartKey={chartConfig.key}
@@ -129,7 +132,7 @@ const DashboardPreview = (props: DashboardPreviewProps) => {
129132
[dataSource, editing, layoutType, state.layout.type]
130133
);
131134

132-
if (layoutType === "tiles") {
135+
if (layoutType === "canvas") {
133136
return (
134137
<ChartPanelLayout
135138
chartConfigs={state.chartConfigs}
@@ -206,19 +209,86 @@ const DashboardPreview = (props: DashboardPreviewProps) => {
206209
);
207210
};
208211

209-
type DndChartPreviewProps = ChartWrapperProps & {
212+
type CommonChartPreviewProps = ChartWrapperProps & {
210213
chartKey: string;
211214
dataSource: DataSource;
212215
};
213216

214-
type ReactGridChartPreviewProps = ChartWrapperProps & {
217+
const ChartPreviewChartMoreButton = ({ chartKey }: { chartKey: string }) => {
218+
const [anchor, setAnchor] = useState<HTMLElement | null>(null);
219+
const handleClose = useEventCallback(() => setAnchor(null));
220+
const [state, dispatch] = useConfiguratorState(hasChartConfigs);
221+
return (
222+
<>
223+
<IconButton onClick={(ev) => setAnchor(ev.currentTarget)}>
224+
<SvgIcMore />
225+
</IconButton>
226+
<ArrowMenu
227+
open={!!anchor}
228+
anchorEl={anchor}
229+
onClose={handleClose}
230+
anchorOrigin={{ horizontal: "center", vertical: "bottom" }}
231+
transformOrigin={{ horizontal: "center", vertical: "top" }}
232+
>
233+
<MenuActionItem
234+
type="button"
235+
as="menuitem"
236+
onClick={() => {
237+
dispatch({ type: "CONFIGURE_CHART", value: { chartKey } });
238+
handleClose();
239+
}}
240+
iconName="edit"
241+
label={<Trans id="chart-controls.edit">Edit</Trans>}
242+
/>
243+
{state.chartConfigs.length > 1 ? (
244+
<MenuActionItem
245+
type="button"
246+
as="menuitem"
247+
color="error"
248+
requireConfirmation
249+
confirmationTitle={t({
250+
id: "chart-controls.delete.title",
251+
message: "Delete chart?",
252+
})}
253+
confirmationText={t({
254+
id: "chart-controls.delete.confirmation",
255+
message: "Are you sure you want to delete this chart?",
256+
})}
257+
onClick={() => {
258+
dispatch({ type: "CHART_CONFIG_REMOVE", value: { chartKey } });
259+
handleClose();
260+
}}
261+
iconName="trash"
262+
label={<Trans id="chart-controls.delete">Delete</Trans>}
263+
/>
264+
) : null}
265+
</ArrowMenu>
266+
</>
267+
);
268+
};
269+
270+
const ChartTopRightControls = ({
271+
chartKey,
272+
dragHandleProps,
273+
}: {
215274
chartKey: string;
216-
dataSource: DataSource;
275+
dragHandleProps?: DragHandleProps;
276+
}) => {
277+
return (
278+
<>
279+
<ChartPreviewChartMoreButton chartKey={chartKey} />
280+
<DragHandle
281+
dragging
282+
className={chartPanelLayoutGridClasses.dragHandle}
283+
{...dragHandleProps}
284+
/>
285+
</>
286+
);
217287
};
218288

219289
const ReactGridChartPreview = forwardRef<
220290
HTMLDivElement,
221-
ReactGridChartPreviewProps
291+
CommonChartPreviewProps
222292
>((props, ref) => {
223293
const { children, chartKey, dataSource, ...rest } = props;
224294
return (
@@ -227,12 +297,7 @@ const ReactGridChartPreview = forwardRef<
227297
<ChartPreviewInner
228298
dataSource={dataSource}
229299
chartKey={chartKey}
230-
actionElementSlot={
231-
<DragHandle
232-
dragging
233-
className={chartPanelLayoutGridClasses.dragHandle}
234-
/>
235-
}
300+
actionElementSlot={<ChartTopRightControls chartKey={chartKey} />}
236301
>
237302
{children}
238303
</ChartPreviewInner>
@@ -241,7 +306,7 @@ const ReactGridChartPreview = forwardRef<
241306
);
242307
});
243308

244-
const DndChartPreview = (props: DndChartPreviewProps) => {
309+
const DndChartPreview = (props: CommonChartPreviewProps) => {
245310
const { children, chartKey, dataSource, ...rest } = props;
246311
const theme = useTheme();
247312
const {
@@ -285,10 +350,13 @@ const DndChartPreview = (props: DndChartPreviewProps) => {
285350
dataSource={dataSource}
286351
chartKey={chartKey}
287352
actionElementSlot={
288-
<DragHandle
289-
{...listeners}
290-
ref={setActivatorNodeRef}
291-
dragging={isDragging}
353+
<ChartTopRightControls
354+
chartKey={chartKey}
355+
dragHandleProps={{
356+
...listeners,
357+
ref: setActivatorNodeRef,
358+
dragging: isDragging,
359+
}}
292360
/>
293361
}
294362
/>
@@ -317,24 +385,27 @@ const SingleURLsPreview = (props: SingleURLsPreviewProps) => {
317385
dataSource={dataSource}
318386
chartKey={chartConfig.key}
319387
actionElementSlot={
320-
<Checkbox
321-
checked={checked}
322-
disabled={keys.length === 1 && checked}
323-
onChange={() => {
324-
dispatch({
325-
type: "LAYOUT_CHANGED",
326-
value: {
327-
...layout,
328-
publishableChartKeys: checked
329-
? keys.filter((k) => k !== key)
330-
: state.chartConfigs
331-
.map((c) => c.key)
332-
.filter((k) => keys.includes(k) || k === key),
333-
},
334-
});
335-
}}
336-
label=""
337-
/>
388+
<>
389+
<ChartPreviewChartMoreButton chartKey={key} />
390+
<Checkbox
391+
checked={checked}
392+
disabled={keys.length === 1 && checked}
393+
onChange={() => {
394+
dispatch({
395+
type: "LAYOUT_CHANGED",
396+
value: {
397+
...layout,
398+
publishableChartKeys: checked
399+
? keys.filter((k) => k !== key)
400+
: state.chartConfigs
401+
.map((c) => c.key)
402+
.filter((k) => keys.includes(k) || k === key),
403+
},
404+
});
405+
}}
406+
label=""
407+
/>
408+
</>
338409
}
339410
/>
340411
</ChartWrapper>

0 commit comments

Comments
 (0)