diff --git a/CHANGELOG.md b/CHANGELOG.md
index f528ffaec..0f57f6e1c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
multiple tabs with visualize.admin or when you bookmark things. [#331](https://github.com/visualize-admin/visualization-tool/pull/331)
- Theme and organization navigation counts take into account the search field now. [#329](https://github.com/visualize-admin/visualization-tool/pull/329)
- Added rivers and lakes through vector layer [#309](https://github.com/visualize-admin/visualization-tool/pull/309)
+- Improved chart editing navigation [#337](https://github.com/visualize-admin/visualization-tool/pull/337)
+- Improved chart publish action buttons [#337](https://github.com/visualize-admin/visualization-tool/pull/337)
### Bugs
diff --git a/app/components/Stack.tsx b/app/components/Stack.tsx
index ed9a8476c..f450ef80f 100644
--- a/app/components/Stack.tsx
+++ b/app/components/Stack.tsx
@@ -1,6 +1,8 @@
import React from "react";
import { Box, BoxProps } from "theme-ui";
+type DirectionType = "row" | "column";
+
const Stack = ({
children,
direction,
@@ -8,9 +10,10 @@ const Stack = ({
...boxProps
}: {
children: React.ReactNode;
- direction?: "row" | "column";
+ direction?: DirectionType | DirectionType[];
spacing?: number;
} & BoxProps) => {
+ const directions = Array.isArray(direction) ? direction : [direction];
return (
* + *:not(html)": {
- [direction === "row" ? "ml" : "mt"]: spacing,
+ ml: directions.map((d) => (d === "row" ? spacing : 0)),
+ mt: directions.map((d) => (d !== "row" ? spacing : 0)),
},
...boxProps.sx,
}}
diff --git a/app/components/chart-panel.tsx b/app/components/chart-panel.tsx
index 1774b318e..a721a57b6 100644
--- a/app/components/chart-panel.tsx
+++ b/app/components/chart-panel.tsx
@@ -11,6 +11,8 @@ export const ChartPanel = ({
sx={{
bg: "monochrome100",
boxShadow: "primary",
+ borderRadius: "xl",
+ overflow: "hidden",
width: "auto",
minHeight: [150, 300, 500],
borderWidth: "1px",
diff --git a/app/components/header.tsx b/app/components/header.tsx
index cba30f4fa..58e510e67 100644
--- a/app/components/header.tsx
+++ b/app/components/header.tsx
@@ -3,6 +3,59 @@ import { Trans } from "@lingui/macro";
import { Box, Flex, Text } from "theme-ui";
import { LanguageMenu } from "./language-menu";
import NextLink from "next/link";
+import React, {
+ Dispatch,
+ SetStateAction,
+ useContext,
+ useMemo,
+ useState,
+} from "react";
+
+const DEFAULT_HEADER_PROGRESS = 100;
+
+export const useHeaderProgressContext = () => {
+ const [value, setValue] = useState(DEFAULT_HEADER_PROGRESS);
+ return useMemo(() => ({ value, setValue }), [value, setValue]);
+};
+
+export const useHeaderProgress = () => useContext(HeaderProgressContext);
+
+const HeaderProgressContext = React.createContext({
+ value: DEFAULT_HEADER_PROGRESS,
+ setValue: (() => undefined) as Dispatch>,
+});
+
+export const HeaderProgressProvider = ({
+ children,
+}: {
+ children: React.ReactNode;
+}) => {
+ const headerProgress = useHeaderProgressContext();
+ return (
+
+ {children}
+
+ );
+};
+
+export const HeaderBorder = () => {
+ const { value: progress } = useHeaderProgress();
+ return (
+
+ );
+};
export const Header = ({
pageType = "app",
@@ -12,33 +65,11 @@ export const Header = ({
contentId?: string;
}) => {
return (
-
-
-
-
+
+
+
+
+
+
);
};
diff --git a/app/components/layout.tsx b/app/components/layout.tsx
index f14193f54..fddb22e2c 100644
--- a/app/components/layout.tsx
+++ b/app/components/layout.tsx
@@ -1,19 +1,21 @@
import { Flex } from "theme-ui";
import { ReactNode } from "react";
import { Footer } from "./footer";
-import { Header } from "./header";
+import { Header, HeaderProgressProvider } from "./header";
export const AppLayout = ({ children }: { children?: ReactNode }) => (
-
-
- {children}
-
+
+
+
+ {children}
+
+
);
diff --git a/app/components/publish-actions.tsx b/app/components/publish-actions.tsx
index 4f2f2e0df..cb528208c 100644
--- a/app/components/publish-actions.tsx
+++ b/app/components/publish-actions.tsx
@@ -1,5 +1,14 @@
import { t, Trans } from "@lingui/macro";
-import { Box, Button, Flex, Input, Link, Text } from "theme-ui";
+import {
+ Box,
+ Button,
+ ButtonProps,
+ Flex,
+ FlexOwnProps,
+ Input,
+ Link,
+ Text,
+} from "theme-ui";
import * as clipboard from "clipboard-polyfill/text";
import Downshift, { DownshiftState, StateChangeOptions } from "downshift";
import {
@@ -8,31 +17,35 @@ import {
useEffect,
useState,
} from "react";
-import { Icon, IconName } from "../icons";
+import { Icon } from "../icons";
import { useLocale } from "../locales/use-locale";
import { IconLink } from "./links";
import { useI18n } from "../lib/use-i18n";
+import Stack from "./Stack";
-export const PublishActions = ({ configKey }: { configKey: string }) => {
+export const PublishActions = ({
+ configKey,
+ sx,
+}: {
+ configKey: string;
+ sx?: FlexOwnProps["sx"];
+}) => {
const locale = useLocale();
return (
-
- {/* */}
+
-
+
);
};
const PopUp = ({
- triggerLabel,
- triggerIconName,
children,
+ renderTrigger,
}: {
- triggerLabel: string | ReactNode;
- triggerIconName: IconName;
children: ReactNode;
+ renderTrigger: (toggleProps: ButtonProps) => React.ReactNode;
}) => {
const [menuIsOpen, toggle] = useState(false);
const handleOuterClick = () => {
@@ -75,44 +88,10 @@ const PopUp = ({
isOpen={menuIsOpen}
>
{({ getToggleButtonProps, getMenuProps, isOpen }) => (
-
-
-
-
{isOpen ? children : null}
-
+
+ {renderTrigger(getToggleButtonProps())}
+ {isOpen ? children : null}
+
)}
);
@@ -126,8 +105,16 @@ export const Share = ({ configKey, locale }: EmbedShareProps) => {
}, [configKey, locale]);
return (
Share}
- triggerIconName="linkExternal"
+ renderTrigger={(props) => {
+ return (
+
+ );
+ }}
>
<>
@@ -230,8 +217,14 @@ export const Embed = ({ configKey, locale }: EmbedShareProps) => {
return (
Embed}
- triggerIconName="embed"
+ renderTrigger={(toggleProps) => (
+
+ )}
>
<>
@@ -345,23 +338,6 @@ const CopyToClipboardTextInput = ({ iFrameCode }: { iFrameCode: string }) => {
);
};
-// export const ImageDownload = () => {
-// const handleClick = () => {
-// console.log("download image");
-// };
-// return (
-//
-// );
-// };
-
-// Presentational Components
-
-// Modal
const PublishActionModal = ({ children }: { children: ReactNode }) => (
{
- const [state, dispatch] = useConfiguratorState();
- const locale = useLocale();
- const [{ data }] = useDataCubeMetadataWithComponentValuesQuery({
- variables: { iri: dataSetIri ?? "", locale },
- });
-
- const goNext = useCallback(() => {
- if (data?.dataCubeByIri) {
- dispatch({
- type: "STEP_NEXT",
- dataSetMetadata: data?.dataCubeByIri,
- });
- }
- }, [data, dispatch]);
-
- const goPrevious = useCallback(() => {
- dispatch({
- type: "STEP_PREVIOUS",
- });
- }, [dispatch]);
-
- const nextDisabled =
- !canTransitionToNextStep(state, data?.dataCubeByIri) ||
- state.state === "PUBLISHING";
- const previousDisabled =
- !canTransitionToPreviousStep(state) || state.state === "PUBLISHING";
-
- const previousLabel = Previous;
- const nextLabel =
- state.state === "DESCRIBING_CHART" || state.state === "PUBLISHING" ? (
- Publish
- ) : (
- Next
- );
-
- return (
-
- {state.state === "SELECTING_CHART_TYPE" ? (
-
- ) : (
- <>
-
-
- >
- )}
-
- );
-};
-
-const NextButton = ({
- label,
- onClick,
- disabled,
-}: {
- label: string | ReactNode;
- onClick: () => void;
- disabled: boolean;
-}) => (
-
-);
diff --git a/app/configurator/components/configurator.tsx b/app/configurator/components/configurator.tsx
index 2f89477b7..484f1576f 100644
--- a/app/configurator/components/configurator.tsx
+++ b/app/configurator/components/configurator.tsx
@@ -1,18 +1,11 @@
-import { Trans } from "@lingui/macro";
-import NextLink from "next/link";
import React from "react";
-import { Link } from "theme-ui";
-import { ConfiguratorState, useConfiguratorState } from "..";
+import { useConfiguratorState } from "..";
import { ChartPanel } from "../../components/chart-panel";
import { ChartPreview } from "../../components/chart-preview";
-import Stack from "../../components/Stack";
-import { useDataCubeMetadataQuery } from "../../graphql/query-hooks";
-import { useLocale } from "../../src";
import { ChartConfiguratorTable } from "../table/table-chart-configurator";
import { ChartAnnotationsSelector } from "./chart-annotations-selector";
import { ChartAnnotator } from "./chart-annotator";
import { ChartConfigurator } from "./chart-configurator";
-import { SectionTitle } from "./chart-controls/section";
import { ChartOptionsSelector } from "./chart-options-selector";
import { ChartTypeSelector } from "./chart-type-selector";
import {
@@ -25,33 +18,6 @@ import {
import { SelectDatasetStep } from "./select-dataset-step";
import { Stepper } from "./stepper";
-const DatasetSelector = ({ state }: { state: ConfiguratorState }) => {
- const locale = useLocale();
- const [{ data: metaData }] = useDataCubeMetadataQuery({
- variables: { iri: state.dataSet || "", locale },
- pause: !state.dataSet,
- });
- return (
-
-
Dataset
- {metaData ? (
-
- {metaData.dataCubeByIri?.title}
-
-
-
-
- Choose another dataset
-
-
-
-
-
- ) : null}
-
- );
-};
-
const SelectChartTypeStep = () => {
const [state] = useConfiguratorState();
if (state.state !== "SELECTING_CHART_TYPE") {
@@ -60,10 +26,7 @@ const SelectChartTypeStep = () => {
return (
<>
-
-
-
-
+
diff --git a/app/configurator/components/stepper.tsx b/app/configurator/components/stepper.tsx
index c680c8105..a2760fa25 100644
--- a/app/configurator/components/stepper.tsx
+++ b/app/configurator/components/stepper.tsx
@@ -1,10 +1,16 @@
import { Trans } from "@lingui/macro";
-import { Fragment, ReactNode, useCallback, useMemo } from "react";
-import { Box, Button, Flex, Text } from "theme-ui";
-import { useConfiguratorState } from "..";
-import { Icon } from "../../icons";
-import { useTheme } from "../../themes";
-import { ActionBar } from "./action-bar";
+import React, { ReactNode, useCallback, useEffect } from "react";
+import { Button, ButtonProps, Flex, Text } from "theme-ui";
+import {
+ useConfiguratorState,
+ canTransitionToNextStep,
+ canTransitionToPreviousStep,
+} from "..";
+import { useHeaderProgress } from "../../components/header";
+import { useDataCubeMetadataWithComponentValuesQuery } from "../../graphql/query-hooks";
+import SvgIcChevronLeft from "../../icons/components/IcChevronLeft";
+import SvgIcChevronRight from "../../icons/components/IcChevronRight";
+import { useLocale } from "../../src";
export type StepStatus = "past" | "current" | "future";
@@ -15,218 +21,216 @@ const steps = [
] as const;
type StepState = typeof steps[number];
-export const Stepper = ({ dataSetIri }: { dataSetIri?: string }) => {
- const [{ state }, dispatch] = useConfiguratorState();
-
- return useMemo(() => {
- const currentStepIndex = steps.indexOf(state as $IntentionalAny);
- return (
-
- {/* Stepper container */}
-
- {steps.map((step, i) => (
-
- i || state === "PUBLISHING"
- ? "past"
- : "future"
- }
- />
- {i < steps.length - 1 && (
- // Connection line
-
- )}
-
- ))}
-
+export const StepperDumb = ({
+ goPrevious,
+ goNext,
+ state,
+ data,
+}: {
+ goPrevious: () => void;
+ goNext: () => void;
+ state: ReturnType[0];
+ data: ReturnType<
+ typeof useDataCubeMetadataWithComponentValuesQuery
+ >[0]["data"];
+}) => {
+ const nextDisabled =
+ !canTransitionToNextStep(state, data?.dataCubeByIri) ||
+ state.state === "PUBLISHING";
+ const previousDisabled =
+ !canTransitionToPreviousStep(state) || state.state === "PUBLISHING";
-
-
+ const previousLabel = Back;
+ const nextLabel =
+ state.state === "DESCRIBING_CHART" || state.state === "PUBLISHING" ? (
+ Publish this visualization
+ ) : (
+ Next
);
- }, [state, dataSetIri]);
-};
-export const Step = ({
- stepState,
- stepNumber,
- status,
-}: {
- stepState: StepState;
- stepNumber: number;
- status: StepStatus;
-}) => {
- const theme = useTheme();
- const [{ dataSet }, dispatch] = useConfiguratorState();
+ const currentStepIndex = steps.indexOf(state.state as $IntentionalAny);
+ const { value: progress, setValue: setProgress } = useHeaderProgress();
+ useEffect(() => {
+ const run = async () => {
+ if (
+ (currentStepIndex === 0 || currentStepIndex === -1) &&
+ progress === 100
+ ) {
+ setProgress(0);
+ await new Promise((resolve) => setTimeout(resolve, 100));
+ }
+ setProgress(
+ Math.round(((currentStepIndex + 1) / (steps.length + 1)) * 100)
+ );
+ };
+ run();
+ return () => {
+ setProgress(100);
+ };
+ }, [currentStepIndex, setProgress, progress]);
- const onClick = useCallback(() => {
- if (status === "past" && dispatch) {
- dispatch({
- type: "STEP_PREVIOUS",
- to: stepState,
- });
- }
- }, [status, stepState, dispatch]);
+ return (
+
+ bg: "monochrome100",
+ borderBottomWidth: "1px",
+ borderBottomStyle: "solid",
+ borderBottomColor: "monochrome500",
+ overflow: "hidden",
+ }}
+ >
+ {/* Stepper container */}
- {/* Icon */}
-
+
+
+ {previousLabel}
+ >
+ }
+ onClick={goPrevious}
+ disabled={previousDisabled}
+ variant="inline-bold"
+ />
+
- bg:
- status === "past"
- ? "monochrome700"
- : status === "current"
- ? "brand"
- : "monochrome600",
- }}
+
- {status === "past" ? (
-
- ) : (
- stepNumber
- )}
+
+
+
+
+ {nextLabel}{" "}
+ {state.state === "DESCRIBING_CHART" ? null : (
+
+ )}
+ >
+ }
+ onClick={goNext}
+ disabled={nextDisabled}
+ variant={
+ state.state === "DESCRIBING_CHART"
+ ? "primary-small"
+ : "inline-bold"
+ }
+ />
-
-
- >
+
);
+};
+
+export const Stepper = ({ dataSetIri }: { dataSetIri?: string }) => {
+ const [state, dispatch] = useConfiguratorState();
+ const locale = useLocale();
+ const [{ data }] = useDataCubeMetadataWithComponentValuesQuery({
+ variables: { iri: dataSetIri ?? "", locale },
+ });
+ const goNext = useCallback(() => {
+ if (data?.dataCubeByIri) {
+ dispatch({
+ type: "STEP_NEXT",
+ dataSetMetadata: data?.dataCubeByIri,
+ });
+ }
+ }, [data, dispatch]);
+
+ const goPrevious = useCallback(() => {
+ dispatch({
+ type: "STEP_PREVIOUS",
+ });
+ }, [dispatch]);
return (
-
+
);
};
-export const StepLabel = ({
- stepState,
- highlight,
-}: {
- stepState: StepState;
- highlight: boolean;
-}) => {
+export const CallToAction = ({ stepState }: { stepState: StepState }) => {
switch (stepState) {
case "SELECTING_CHART_TYPE":
return (
- Visualization Type}
- highlight={highlight}
+
+ Select the desired chart type for your dataset.
+
+ }
/>
);
case "CONFIGURING_CHART":
return (
- Visualize}
- highlight={highlight}
+
+ Customize your visualization by using the filter and color
+ segmentation options in the sidebars.
+
+ }
/>
);
case "DESCRIBING_CHART":
return (
- Annotate}
- highlight={highlight}
+
+ Before publishing, add a title and description to your chart, and
+ choose which elements should be interactive.
+
+ }
/>
);
}
return null;
};
-const StepLabelText = ({
- highlight,
- label,
-}: {
- highlight: boolean;
- label: ReactNode;
-}) => {
+const CallToActionText = ({ label }: { label: ReactNode }) => {
return (
- <>
- {/* Add background colored bold label underneath the actual
- label, to avoid changing container's size when the text becomes bold. */}
-
- {label}
-
- {label}
-
-
- >
+
+ {label}
+
);
};
+
+const NavButton = ({
+ label,
+ onClick,
+ disabled,
+ ...props
+}: {
+ label: string | ReactNode;
+ onClick: () => void;
+ disabled: boolean;
+} & ButtonProps) => (
+
+);
diff --git a/app/configurator/configurator-state.tsx b/app/configurator/configurator-state.tsx
index 628338f17..f5fd24e71 100644
--- a/app/configurator/configurator-state.tsx
+++ b/app/configurator/configurator-state.tsx
@@ -965,7 +965,7 @@ const reducer: Reducer = (
}
};
-const ConfiguratorStateContext = createContext<
+export const ConfiguratorStateContext = createContext<
[ConfiguratorState, Dispatch] | undefined
>(undefined);
diff --git a/app/docs/action-bar.docs.tsx b/app/docs/action-bar.docs.tsx
deleted file mode 100644
index 306c69f34..000000000
--- a/app/docs/action-bar.docs.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import { markdown, ReactSpecimen } from "catalog";
-import { Box, Text } from "theme-ui";
-import { ConfiguratorStateProvider } from "../configurator";
-import { ActionBar } from "../configurator/components/action-bar";
-import { states } from "./fixtures";
-
-export default () =>
- markdown`
-> The action bar is a container for components that trigger actions.
-
-## How to use
-
-~~~
-import { ActionBar } from "./components/action-bar"
-
-
- {children}
-
-~~~
-
-## Action Bar in different states
-
-${states.map((state) => (
-
- {state.state}
-
-
-
-
-
-
-))}
-`;
diff --git a/app/docs/button.docs.tsx b/app/docs/button.docs.tsx
index 75e3c979b..58d7961da 100644
--- a/app/docs/button.docs.tsx
+++ b/app/docs/button.docs.tsx
@@ -1,20 +1,28 @@
import { Button } from "theme-ui";
import { markdown, ReactSpecimen } from "catalog";
+import { Icon } from "../icons";
+import SvgIcChevronRight from "../icons/components/IcChevronRight";
+import SvgIcChevronLeft from "../icons/components/IcChevronLeft";
export default () => markdown`
> Buttons are used to trigger an event after a user interaction.
-There are four basic styles that are styles defined in \`rebass\`'s \`variants\`:
+There are four basic styles that are styles defined in \`theme-ui\`'s \`variants\`:
- \`primary\`
+- \`primary-small\`
- \`secondary\`
- \`success\`
-- \`outline\`
+- \`inline\`
+- \`inline-bold\`
${(
-
+
)}
@@ -36,11 +44,37 @@ There are four basic styles that are styles defined in \`rebass\`'s \`variants\`
)}
+ ${(
+
+
+
+ )}
+ ${(
+
+
+
+ )}
+ ${(
+
+
+
+ )}
## How to use
~~~
import { Button } from "theme-ui"
+import SvgIcChevronRight from "../icons/components/IcChevronRight";
+import SvgIcChevronLeft from "../icons/components/IcChevronLeft";