Skip to content

Commit

Permalink
Merge pull request #1541 from visualize-admin/feat/dashboard-action-menu
Browse files Browse the repository at this point in the history
Dashboard action menu
  • Loading branch information
ptbrowne authored May 29, 2024
2 parents 072a53d + 4c43c46 commit 86d242f
Show file tree
Hide file tree
Showing 26 changed files with 660 additions and 335 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

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

# Unreleased

- Features
- Add the "Free canvas" layout, allowing users to freely resize and move charts for dashboards
- Ability to start a chart from another dataset than the current one

# [4.5.1] - 2024-05-21

- Fixes
- If there is an error on a chart contained in a dashboard, the layout is not be broken

# [4.5.0] - 2024-05-21

- Features
Expand Down
17 changes: 8 additions & 9 deletions app/components/chart-panel-layout-grid.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import clsx from "clsx";
import { fold } from "fp-ts/lib/Either";
import { pipe } from "fp-ts/lib/function";
import { useState } from "react";
import { Layouts } from "react-grid-layout";

import { ChartPanelLayoutTypeProps } from "@/components/chart-panel";
import ChartGridLayout from "@/components/react-grid";
import { ReactGridLayoutsType, isLayouting } from "@/configurator";
import { isLayouting, ReactGridLayoutsType } from "@/configurator";
import { useConfiguratorState } from "@/src";
import { assert } from "@/utils/assert";

Expand All @@ -30,13 +29,13 @@ const decodeLayouts = (layouts: Layouts) => {
);
};

const ChartPanelLayoutGrid = (props: ChartPanelLayoutTypeProps) => {
const ChartPanelLayoutCanvas = (props: ChartPanelLayoutTypeProps) => {
const { chartConfigs } = props;
const [config, dispatch] = useConfiguratorState(isLayouting);
const [layouts, setLayouts] = useState<Layouts>(() => {
assert(
config.layout.type === "dashboard" && config.layout.layout === "tiles",
"ChartPanelLayoutGrid should be rendered only for dashboard layout with tiles"
config.layout.type === "dashboard" && config.layout.layout === "canvas",
"ChartPanelLayoutGrid should be rendered only for dashboard layout with canvas"
);

return config.layout.layouts;
Expand All @@ -45,8 +44,8 @@ const ChartPanelLayoutGrid = (props: ChartPanelLayoutTypeProps) => {
const handleChangeLayouts = (layouts: Layouts) => {
const layout = config.layout;
assert(
layout.type === "dashboard" && layout.layout === "tiles",
"ChartPanelLayoutGrid should be rendered only for dashboard layout with tiles"
layout.type === "dashboard" && layout.layout === "canvas",
"ChartPanelLayoutGrid should be rendered only for dashboard layout with canvas"
);

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

return (
<ChartGridLayout
className={clsx("layout", chartPanelLayoutGridClasses.root)}
className={chartPanelLayoutGridClasses.root}
layouts={layouts}
resize
draggableHandle={`.${chartPanelLayoutGridClasses.dragHandle}`}
Expand All @@ -77,4 +76,4 @@ const ChartPanelLayoutGrid = (props: ChartPanelLayoutTypeProps) => {
);
};

export default ChartPanelLayoutGrid;
export default ChartPanelLayoutCanvas;
21 changes: 17 additions & 4 deletions app/components/chart-panel.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { Box, BoxProps, Theme } from "@mui/material";
import { makeStyles } from "@mui/styles";
import clsx from "clsx";
import React, { HTMLProps, forwardRef } from "react";
import React, { forwardRef, HTMLProps } from "react";

import ChartPanelLayoutGrid from "@/components/chart-panel-layout-grid";
import ChartPanelLayoutCanvas, {
chartPanelLayoutGridClasses,
} from "@/components/chart-panel-layout-grid";
import { ChartPanelLayoutTall } from "@/components/chart-panel-layout-tall";
import { ChartPanelLayoutVertical } from "@/components/chart-panel-layout-vertical";
import { ChartSelectionTabs } from "@/components/chart-selection-tabs";
Expand All @@ -15,6 +17,17 @@ const useStyles = makeStyles((theme: Theme) => ({
flexDirection: "column",
gap: theme.spacing(4),
},
chartWrapper: {
[`.${chartPanelLayoutGridClasses.root} &`]: {
transition: theme.transitions.create(["box-shadow"], {
duration: theme.transitions.duration.shortest,
}),
},
[`.${chartPanelLayoutGridClasses.root} &:has(.${chartPanelLayoutGridClasses.dragHandle}:hover)`]:
{
boxShadow: theme.shadows[6],
},
},
chartWrapperInner: {
display: "contents",
overflow: "hidden",
Expand All @@ -33,7 +46,7 @@ export const ChartWrapper = forwardRef<HTMLDivElement, ChartWrapperProps>(
const { children, editing, layoutType, ...rest } = props;
const classes = useStyles();
return (
<Box ref={ref} {...rest}>
<Box ref={ref} {...rest} className={classes.chartWrapper}>
{(editing || layoutType === "tab") && <ChartSelectionTabs />}
<Box
className={classes.chartWrapperInner}
Expand Down Expand Up @@ -64,7 +77,7 @@ const Wrappers: Record<
> = {
vertical: ChartPanelLayoutVertical,
tall: ChartPanelLayoutTall,
tiles: ChartPanelLayoutGrid,
canvas: ChartPanelLayoutCanvas,
};

export const ChartPanelLayout = (props: ChartPanelLayoutProps) => {
Expand Down
147 changes: 109 additions & 38 deletions app/components/chart-preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import {
useDraggable,
useDroppable,
} from "@dnd-kit/core";
import { Trans } from "@lingui/macro";
import { Box } from "@mui/material";
import { t, Trans } from "@lingui/macro";
import { Box, IconButton, useEventCallback } from "@mui/material";
import Head from "next/head";
import React, {
forwardRef,
Expand All @@ -20,6 +20,7 @@ import React, {
import { DataSetTable } from "@/browse/datatable";
import { ChartDataFilters } from "@/charts/shared/chart-data-filters";
import { LoadingStateProvider } from "@/charts/shared/chart-loading-state";
import { ArrowMenu } from "@/components/arrow-menu";
import { ChartErrorBoundary } from "@/components/chart-error-boundary";
import { ChartFootnotes } from "@/components/chart-footnotes";
import {
Expand All @@ -35,10 +36,11 @@ import {
import { useChartStyles } from "@/components/chart-utils";
import { ChartWithFilters } from "@/components/chart-with-filters";
import DebugPanel from "@/components/debug-panel";
import { DragHandle } from "@/components/drag-handle";
import { DragHandle, DragHandleProps } from "@/components/drag-handle";
import Flex from "@/components/flex";
import { Checkbox } from "@/components/form";
import { HintYellow } from "@/components/hint";
import { MenuActionItem } from "@/components/menu-action-item";
import { MetadataPanel } from "@/components/metadata-panel";
import { BANNER_MARGIN_TOP } from "@/components/presence";
import {
Expand All @@ -56,6 +58,7 @@ import {
useDataCubesMetadataQuery,
} from "@/graphql/hooks";
import { DataCubePublicationStatus } from "@/graphql/resolver-types";
import SvgIcMore from "@/icons/components/IcMore";
import { useLocale } from "@/locales/use-locale";
import { InteractiveFiltersChartProvider } from "@/stores/interactive-filters";
import { useTransitionStore } from "@/stores/transition";
Expand Down Expand Up @@ -109,7 +112,7 @@ const DashboardPreview = (props: DashboardPreviewProps) => {
const [over, setOver] = useState<Over | null>(null);
const renderChart = useCallback(
(chartConfig: ChartConfig) => {
return layoutType === "tiles" ? (
return layoutType === "canvas" ? (
<ReactGridChartPreview
key={chartConfig.key}
chartKey={chartConfig.key}
Expand All @@ -129,7 +132,7 @@ const DashboardPreview = (props: DashboardPreviewProps) => {
[dataSource, editing, layoutType, state.layout.type]
);

if (layoutType === "tiles") {
if (layoutType === "canvas") {
return (
<ChartPanelLayout
chartConfigs={state.chartConfigs}
Expand Down Expand Up @@ -206,19 +209,86 @@ const DashboardPreview = (props: DashboardPreviewProps) => {
);
};

type DndChartPreviewProps = ChartWrapperProps & {
type CommonChartPreviewProps = ChartWrapperProps & {
chartKey: string;
dataSource: DataSource;
};

type ReactGridChartPreviewProps = ChartWrapperProps & {
const ChartPreviewChartMoreButton = ({ chartKey }: { chartKey: string }) => {
const [anchor, setAnchor] = useState<HTMLElement | null>(null);
const handleClose = useEventCallback(() => setAnchor(null));
const [state, dispatch] = useConfiguratorState(hasChartConfigs);
return (
<>
<IconButton onClick={(ev) => setAnchor(ev.currentTarget)}>
<SvgIcMore />
</IconButton>
<ArrowMenu
open={!!anchor}
anchorEl={anchor}
onClose={handleClose}
anchorOrigin={{ horizontal: "center", vertical: "bottom" }}
transformOrigin={{ horizontal: "center", vertical: "top" }}
>
<MenuActionItem
type="button"
as="menuitem"
onClick={() => {
dispatch({ type: "CONFIGURE_CHART", value: { chartKey } });
handleClose();
}}
iconName="edit"
label={<Trans id="chart-controls.edit">Edit</Trans>}
/>
{state.chartConfigs.length > 1 ? (
<MenuActionItem
type="button"
as="menuitem"
color="error"
requireConfirmation
confirmationTitle={t({
id: "chart-controls.delete.title",
message: "Delete chart?",
})}
confirmationText={t({
id: "chart-controls.delete.confirmation",
message: "Are you sure you want to delete this chart?",
})}
onClick={() => {
dispatch({ type: "CHART_CONFIG_REMOVE", value: { chartKey } });
handleClose();
}}
iconName="trash"
label={<Trans id="chart-controls.delete">Delete</Trans>}
/>
) : null}
</ArrowMenu>
</>
);
};

const ChartTopRightControls = ({
chartKey,
dragHandleProps,
}: {
chartKey: string;
dataSource: DataSource;
dragHandleProps?: DragHandleProps;
}) => {
return (
<>
<ChartPreviewChartMoreButton chartKey={chartKey} />
<DragHandle
dragging
className={chartPanelLayoutGridClasses.dragHandle}
{...dragHandleProps}
/>
</>
);
};

const ReactGridChartPreview = forwardRef<
HTMLDivElement,
ReactGridChartPreviewProps
CommonChartPreviewProps
>((props, ref) => {
const { children, chartKey, dataSource, ...rest } = props;
return (
Expand All @@ -227,12 +297,7 @@ const ReactGridChartPreview = forwardRef<
<ChartPreviewInner
dataSource={dataSource}
chartKey={chartKey}
actionElementSlot={
<DragHandle
dragging
className={chartPanelLayoutGridClasses.dragHandle}
/>
}
actionElementSlot={<ChartTopRightControls chartKey={chartKey} />}
>
{children}
</ChartPreviewInner>
Expand All @@ -241,7 +306,7 @@ const ReactGridChartPreview = forwardRef<
);
});

const DndChartPreview = (props: DndChartPreviewProps) => {
const DndChartPreview = (props: CommonChartPreviewProps) => {
const { children, chartKey, dataSource, ...rest } = props;
const theme = useTheme();
const {
Expand Down Expand Up @@ -285,10 +350,13 @@ const DndChartPreview = (props: DndChartPreviewProps) => {
dataSource={dataSource}
chartKey={chartKey}
actionElementSlot={
<DragHandle
{...listeners}
ref={setActivatorNodeRef}
dragging={isDragging}
<ChartTopRightControls
chartKey={chartKey}
dragHandleProps={{
...listeners,
ref: setActivatorNodeRef,
dragging: isDragging,
}}
/>
}
/>
Expand Down Expand Up @@ -317,24 +385,27 @@ const SingleURLsPreview = (props: SingleURLsPreviewProps) => {
dataSource={dataSource}
chartKey={chartConfig.key}
actionElementSlot={
<Checkbox
checked={checked}
disabled={keys.length === 1 && checked}
onChange={() => {
dispatch({
type: "LAYOUT_CHANGED",
value: {
...layout,
publishableChartKeys: checked
? keys.filter((k) => k !== key)
: state.chartConfigs
.map((c) => c.key)
.filter((k) => keys.includes(k) || k === key),
},
});
}}
label=""
/>
<>
<ChartPreviewChartMoreButton chartKey={key} />
<Checkbox
checked={checked}
disabled={keys.length === 1 && checked}
onChange={() => {
dispatch({
type: "LAYOUT_CHANGED",
value: {
...layout,
publishableChartKeys: checked
? keys.filter((k) => k !== key)
: state.chartConfigs
.map((c) => c.key)
.filter((k) => keys.includes(k) || k === key),
},
});
}}
label=""
/>
</>
}
/>
</ChartWrapper>
Expand Down
Loading

0 comments on commit 86d242f

Please sign in to comment.