Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dashboard action menu #1541

Merged
merged 17 commits into from
May 29, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading