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

Save draft #1340

Merged
merged 59 commits into from
Feb 20, 2024
Merged
Show file tree
Hide file tree
Changes from 57 commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
d974f43
fix: Add optional access to navigator.vendor deprecated property
ptbrowne Feb 12, 2024
0294863
docs: Add table of contents
ptbrowne Feb 12, 2024
af8a102
refactor: Extract keys early
ptbrowne Feb 13, 2024
56929a9
refactor: Use query cache & invalidation to facilitate mutation
ptbrowne Feb 13, 2024
aea33b7
feat: Add useMutate helper
ptbrowne Feb 13, 2024
38dbb98
refactor: Remove config through useMutate
ptbrowne Feb 13, 2024
42d977a
feat: Add draft published state
ptbrowne Feb 13, 2024
f81c149
feat: Ability to turn config into draft/publish it from chart list
ptbrowne Feb 13, 2024
c995497
refactor: Extract saveState from component
ptbrowne Feb 13, 2024
3844279
fix: Config for existing chart is correctly fetched
ptbrowne Feb 13, 2024
23dae00
refactor: Rename for clarity
ptbrowne Feb 13, 2024
1d5792c
feat: Use menu instead of tooltip
ptbrowne Feb 13, 2024
64b6359
fix fetch data
ptbrowne Feb 15, 2024
9663353
refactor: Rename method
ptbrowne Feb 15, 2024
4303529
refactor: Decompose effect in both parts
ptbrowne Feb 15, 2024
9d77f58
feat: Show warning if viewing chart still in draft
ptbrowne Feb 15, 2024
ba80167
feat: Add save draft button
ptbrowne Feb 15, 2024
867311c
fix: Add required chartId prop
ptbrowne Feb 15, 2024
af92a08
feat: Add translations for chart features
ptbrowne Feb 15, 2024
22ad15a
refactor: Add docs and explicit the version
ptbrowne Feb 15, 2024
e6f5479
fix: Inverted message
ptbrowne Feb 15, 2024
98d73f7
feat: Add published state as non optional
ptbrowne Feb 15, 2024
dded7a7
fix: button in button
ptbrowne Feb 16, 2024
79786d3
feat: Make create and update config simpler, they take a config and w…
ptbrowne Feb 16, 2024
f32fb11
Show SaveAsDraft button inside layout options
ptbrowne Feb 16, 2024
e51afff
feat: Hide save as draft button if not logged in
ptbrowne Feb 16, 2024
5134a9c
refactor: Rename SaveDraft button
ptbrowne Feb 16, 2024
5000dde
feat: Ability to dismiss snack
ptbrowne Feb 16, 2024
fd452c9
fix: Can save in draft a new chart
ptbrowne Feb 16, 2024
ec1baed
refactor: Reduce case
ptbrowne Feb 16, 2024
a9c297c
refactor: Remove unnnecessary type
ptbrowne Feb 16, 2024
1aa90d2
fix: Remove target blank
ptbrowne Feb 16, 2024
fc0d75b
feat: Show arrow for menu
ptbrowne Feb 16, 2024
6c2842e
fix: Remove config call does not check incoming data from userId, che…
ptbrowne Feb 16, 2024
424bbf2
fix: Gap
ptbrowne Feb 16, 2024
431703f
feat: Show edit button on chart page if it's a draft
ptbrowne Feb 16, 2024
07aded3
refactor: Extract Confirmation Dialog from actions
ptbrowne Feb 16, 2024
ac7621a
feat: Refactor actions to be able to show the primary one as button
ptbrowne Feb 16, 2024
31897ca
fix: Typo
ptbrowne Feb 16, 2024
2097c22
refactor: Split component and make title of visualisation table dynamic
ptbrowne Feb 16, 2024
a0fd5e0
feat: Show drafts and published configs separately
ptbrowne Feb 16, 2024
394ea79
feat: Show actions differently for drafts & published
ptbrowne Feb 16, 2024
2981ece
feat: Show only small check, less things to translate
ptbrowne Feb 16, 2024
8dfdbf2
feat: Make button a bit more lively
ptbrowne Feb 16, 2024
b54f3fd
feat: Add translations
ptbrowne Feb 16, 2024
18a823f
fix: Menu item takes the whole width
ptbrowne Feb 16, 2024
8f5e588
fix: Correct import
ptbrowne Feb 16, 2024
e11e4b9
fix: Missing published state
ptbrowne Feb 16, 2024
d1ebad4
fix: Unused
ptbrowne Feb 16, 2024
6b5846c
refactor: Extract row actions and confirmation dialog from profile-ta…
ptbrowne Feb 19, 2024
e9710a7
feat: Add xsmall variant to button sizes
ptbrowne Feb 19, 2024
4b541df
feat: Show delete in red
ptbrowne Feb 19, 2024
822381f
fix: Make all tables look the same
ptbrowne Feb 19, 2024
dd8bca6
refactor: Extract rename dialog
ptbrowne Feb 20, 2024
ce7c21b
feat: Wrap th cells into table row and extract styling to table
ptbrowne Feb 20, 2024
47a83d6
fix: Delete should correctly work
ptbrowne Feb 20, 2024
75ad1c9
fix: Correctly call confirmation if required
ptbrowne Feb 20, 2024
76b02cf
fix: Typo
ptbrowne Feb 20, 2024
076b30b
fix: Width exceeded 100%
ptbrowne Feb 20, 2024
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
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,37 @@
# Visualization Tool

<!-- vscode-markdown-toc -->

- 1. [Documentation](#Documentation)
- 2. [Development Environment](#DevelopmentEnvironment)
- 2.1. [Setting up the dev environment](#Settingupthedevenvironment)
- 2.2. [Dev server](#Devserver)
- 2.3. [Postgres database](#Postgresdatabase)
- 2.4. [Building the Embed script `/dist/embed.js`](#BuildingtheEmbedscriptdistembed.js)
- 2.4.1. [Database migrations](#Databasemigrations)
- 3. [Versioning](#Versioning)
- 4. [Deployment](#Deployment)
- 4.1. [Heroku](#Heroku)
- 4.2. [Abraxas](#Abraxas)
- 4.3. [Docker (anywhere)](#Dockeranywhere)
- 5. [E2E tests](#E2Etests)
- 6. [GraphQL performance tests](#GraphQLperformancetests)
- 6.1. [Automation](#Automation)
- 6.2. [How to add or modify the tests](#Howtoaddormodifythetests)
- 7. [Load tests](#Loadtests)
- 7.1. [Automation](#Automation-1)
- 7.2. [Local setup](#Localsetup)
- 7.3. [Running the tests locally](#Runningthetestslocally)
- 7.4. [Recording the tests using Playwright](#RecordingthetestsusingPlaywright)
- 8. [Authentication](#Authentication)
- 8.1. [Locally](#Locally)

<!-- vscode-markdown-toc-config
numbering=true
autoSave=true
/vscode-markdown-toc-config -->
<!-- /vscode-markdown-toc -->

ptbrowne marked this conversation as resolved.
Show resolved Hide resolved
## Documentation

A public documentation is available at https://visualize.admin.ch/docs/.
Expand Down
22 changes: 22 additions & 0 deletions app/components/arrow-menu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Menu, paperClasses, styled } from "@mui/material";

export const ArrowMenu = styled(Menu)(({ theme }) => ({
[`& .${paperClasses.root}`]: {
overflowY: "visible",
overflowX: "visible",
"&:before": {
content: '" "',
display: "block",
background: theme.palette.background.paper,
width: 10,
height: 10,
transform: "rotate(45deg)",
position: "absolute",
margin: "auto",
top: -5,

left: 0,
right: 0,
},
},
}));
166 changes: 144 additions & 22 deletions app/components/chart-selection-tabs.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
import { Trans, t } from "@lingui/macro";
import { t, Trans } from "@lingui/macro";
import {
Box,
BoxProps,
Button,
Grow,
Popover,
Tab,
Tabs,
Theme,
Tooltip,
useEventCallback,
} from "@mui/material";
import { makeStyles } from "@mui/styles";
import { PUBLISHED_STATE } from "@prisma/client";
import { useSession } from "next-auth/react";
import { useRouter } from "next/router";
import React from "react";
import { DragDropContext, Draggable, Droppable } from "react-beautiful-dnd";
import { useDebounce } from "use-debounce";

import { extractChartConfigComponentIris } from "@/charts/shared/chart-helpers";
import Flex from "@/components/flex";
Expand All @@ -33,14 +37,17 @@ import {
} from "@/configurator";
import { ChartTypeSelector } from "@/configurator/components/chart-type-selector";
import { getIconName } from "@/configurator/components/ui-helpers";
import { useUserConfig } from "@/domain/user-configs";
import { useDataCubesComponentsQuery } from "@/graphql/hooks";
import { Icon, IconName } from "@/icons";
import { useLocale } from "@/src";
import { fetchChartConfig } from "@/utils/chart-config/api";
import { createConfig, updateConfig } from "@/utils/chart-config/api";
import { createChartId } from "@/utils/create-chart-id";
import { getRouterChartId } from "@/utils/router/helpers";
import useEvent from "@/utils/use-event";
import { useFetchData } from "@/utils/use-fetch-data";
import { useMutate } from "@/utils/use-fetch-data";

import { useLocalSnack } from "./use-local-snack";

type TabsState = {
popoverOpen: boolean;
Expand Down Expand Up @@ -314,22 +321,129 @@ export const LayoutChartButton = () => {
);
};

export const PublishChartButton = () => {
const { asPath } = useRouter();
export const SaveDraftButton = ({
chartId,
}: {
chartId: string | undefined;
}) => {
const { data: config, invalidate: invalidateConfig } = useUserConfig(chartId);
const session = useSession();
const chartId = getRouterChartId(asPath);
const queryFn = React.useCallback(
() => fetchChartConfig(chartId ?? ""),
[chartId]
);
const { data: config, status } = useFetchData(queryFn, {
enable: !!(session.data?.user && chartId),
initialStatus: "fetching",

const [state] = useConfiguratorState();

const [snack, enqueueSnackbar, dismissSnack] = useLocalSnack();
const [debouncedSnack] = useDebounce(snack, 500);
const { asPath, replace } = useRouter();

const createConfigMut = useMutate(createConfig);
const updatePublishedStateMut = useMutate(updateConfig);
const loggedInId = session.data?.user.id;

const handleClick = useEventCallback(async () => {
try {
if (config?.user_id && loggedInId) {
const updated = await updatePublishedStateMut.mutate({
data: state,
user_id: loggedInId,
published_state: PUBLISHED_STATE.DRAFT,
key: config.key,
});

if (updated) {
if (asPath !== `/create/${updated.key}`) {
replace(`/create/new?edit=${updated.key}`);
}
} else {
throw new Error("Could not update draft");
}
} else if (state) {
const saved = await createConfigMut.mutate({
data: state,
user_id: loggedInId,
published_state: PUBLISHED_STATE.DRAFT,
});
if (saved) {
enqueueSnackbar({
message: t({
id: "button.save-draft.saved",
message: "Draft saved",
}),
variant: "success",
});
replace(`/create/new?edit=${saved.key}`);
} else {
throw new Error("Could not save draft");
}
}
invalidateConfig();
} catch (e) {
console.log(
`Error while saving draft: ${e instanceof Error ? e.message : e}`
);
enqueueSnackbar({
message: t({
id: "button.save-draft.error",
message: "Could not save draft",
}),
variant: "error",
});
}

setTimeout(() => {
updatePublishedStateMut.reset();
createConfigMut.reset();
}, 2000);
});

const hasUpdated = !!(updatePublishedStateMut.data || createConfigMut.data);
const [debouncedHasUpdated] = useDebounce(hasUpdated, 300);

if (!loggedInId) {
return null;
}

return (
<Tooltip
arrow
title={debouncedSnack?.message ?? ""}
open={!!snack}
disableFocusListener
disableHoverListener
disableTouchListener
onClose={() => dismissSnack()}
>
<Button
endIcon={
hasUpdated || debouncedHasUpdated ? (
<Grow in={hasUpdated}>
<span>
<Icon name="check" />
</span>
</Grow>
) : null
}
variant="outlined"
onClick={handleClick}
>
<Trans id="button.save-draft">Save draft</Trans>
</Button>
</Tooltip>
);
};

export const PublishChartButton = ({
chartId,
}: {
chartId: string | undefined;
}) => {
const session = useSession();
const { data: config } = useUserConfig(chartId);
const editingPublishedChart =
session.data?.user.id && config?.user_id === session.data.user.id;
session.data?.user.id &&
config?.user_id === session.data.user.id &&
config.published_state === "PUBLISHED";

return status === "fetching" ? null : (
return (
<NextStepButton>
{editingPublishedChart ? (
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
Expand Down Expand Up @@ -375,6 +489,9 @@ const TabsInner = (props: TabsInnerProps) => {
} = props;
const [state, dispatch] = useConfiguratorState(hasChartConfigs);

const { asPath } = useRouter();
const chartId = getRouterChartId(asPath);

return (
<Box
sx={{
Expand Down Expand Up @@ -422,6 +539,7 @@ const TabsInner = (props: TabsInnerProps) => {
{...provided.draggableProps}
{...provided.dragHandleProps}
style={{ ...style, transform, opacity: 1 }}
component="div"
key={d.key}
sx={{
mr: 2,
Expand Down Expand Up @@ -460,6 +578,7 @@ const TabsInner = (props: TabsInnerProps) => {

{addable && (
<Tab
component="div"
sx={{
ml: (theme) => `-${theme.spacing(2)}`,
p: 0,
Expand All @@ -478,13 +597,16 @@ const TabsInner = (props: TabsInnerProps) => {
</Droppable>
</DragDropContext>

{editable &&
isConfiguring(state) &&
(enableLayouting(state) ? (
<LayoutChartButton />
) : (
<PublishChartButton />
))}
<Box gap="0.5rem" display="flex">
{isConfiguring(state) ? <SaveDraftButton chartId={chartId} /> : null}
{editable &&
isConfiguring(state) &&
(enableLayouting(state) ? (
<LayoutChartButton />
) : (
<PublishChartButton chartId={chartId} />
))}
</Box>
</Box>
);
};
Expand Down
84 changes: 84 additions & 0 deletions app/components/confirmation-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { t, Trans } from "@lingui/macro";
import {
Button,
CircularProgress,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogProps,
DialogTitle,
Typography,
} from "@mui/material";
import React from "react";

const ConfirmationDialog = ({
title,
text,
onClick,
onSuccess,
onConfirm,
...props
}: DialogProps & {
title?: string;
text?: string;
onSuccess?: () => Promise<unknown> | void;
onConfirm?: () => Promise<unknown> | void;
onClick: () => Promise<unknown> | void;
}) => {
const [loading, setLoading] = React.useState(false);

return (
<Dialog
// To prevent the click away listener from closing the dialog.
onClick={(e) => e.stopPropagation()}
onClose={close}
maxWidth="xs"
{...props}
>
<DialogTitle>
<Typography variant="h3">
{title ??
t({
id: "login.profile.chart.confirmation.default",
message: "Are you sure you want to perform this action?",
})}
</Typography>
</DialogTitle>
{text && (
<DialogContent>
<DialogContentText>{text}</DialogContentText>
</DialogContent>
)}
<DialogActions
sx={{
"& > .MuiButton-root": {
justifyContent: "center",
pointerEvents: loading ? "none" : "auto",
},
}}
>
<Button variant="text" onClick={close}>
<Trans id="no">No</Trans>
</Button>
<Button
variant="text"
onClick={async (e) => {
e.stopPropagation();
setLoading(true);

await onClick();
await new Promise((r) => setTimeout(r, 100));

props.onClose?.({}, "escapeKeyDown");
onSuccess?.();
}}
>
{loading ? <CircularProgress /> : <Trans id="yes">Yes</Trans>}
</Button>
</DialogActions>
</Dialog>
);
};

export default ConfirmationDialog;
2 changes: 1 addition & 1 deletion app/components/hint.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ export const OnlyNegativeDataHint = () => (
</Alert>
);

export const Success = () => (
export const PublishSuccess = () => (
<Alert severity="success" icon={<Icon name="datasetSuccess" size={64} />}>
<Trans id="hint.publication.success">
Your visualization is now published. You can share and embed it using the
Expand Down
Loading
Loading