feat(web): add exporting flow functionality

This commit is contained in:
Ali BARIN
2025-01-13 12:30:15 +00:00
parent 7d621c07f1
commit ec14801261
8 changed files with 138 additions and 13 deletions

View File

@@ -37,6 +37,7 @@
"slate": "^0.94.1",
"slate-history": "^0.93.0",
"slate-react": "^0.94.2",
"slugify": "^1.6.6",
"uuid": "^9.0.0",
"web-vitals": "^1.0.1",
"yup": "^0.32.11"

View File

@@ -10,25 +10,31 @@ import Snackbar from '@mui/material/Snackbar';
import { ReactFlowProvider } from 'reactflow';
import { EditorProvider } from 'contexts/Editor';
import EditableTypography from 'components/EditableTypography';
import Container from 'components/Container';
import Editor from 'components/Editor';
import Can from 'components/Can';
import useFormatMessage from 'hooks/useFormatMessage';
import * as URLS from 'config/urls';
import { TopBar } from './style';
import * as URLS from 'config/urls';
import Can from 'components/Can';
import Container from 'components/Container';
import EditableTypography from 'components/EditableTypography';
import Editor from 'components/Editor';
import EditorNew from 'components/EditorNew/EditorNew';
import useFlow from 'hooks/useFlow';
import useFormatMessage from 'hooks/useFormatMessage';
import useUpdateFlow from 'hooks/useUpdateFlow';
import useUpdateFlowStatus from 'hooks/useUpdateFlowStatus';
import EditorNew from 'components/EditorNew/EditorNew';
import useExportFlow from 'hooks/useExportFlow';
import useDownloadJsonAsFile from 'hooks/useDownloadJsonAsFile';
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
const useNewFlowEditor = process.env.REACT_APP_USE_NEW_FLOW_EDITOR === 'true';
export default function EditorLayout() {
const { flowId } = useParams();
const formatMessage = useFormatMessage();
const enqueueSnackbar = useEnqueueSnackbar();
const { mutateAsync: updateFlow } = useUpdateFlow(flowId);
const { mutateAsync: updateFlowStatus } = useUpdateFlowStatus(flowId);
const { mutateAsync: exportFlow } = useExportFlow(flowId);
const downloadJsonAsFile = useDownloadJsonAsFile();
const { data, isLoading: isFlowLoading } = useFlow(flowId);
const flow = data?.data;
@@ -38,6 +44,19 @@ export default function EditorLayout() {
});
};
const onExportFlow = async (name) => {
const flowExport = await exportFlow();
downloadJsonAsFile({
contents: flowExport.data,
name: flowExport.data.name,
});
enqueueSnackbar(formatMessage('flowEditor.flowSuccessfullyExported'), {
variant: 'success',
});
};
return (
<>
<TopBar
@@ -79,7 +98,22 @@ export default function EditorLayout() {
)}
</Box>
<Box pr={1}>
<Box pr={1} display="flex" gap={1}>
<Can I="read" a="Flow" passThrough>
{(allowed) => (
<Button
disabled={!allowed || !flow}
variant="contained"
color="info"
size="small"
onClick={onExportFlow}
data-test="export-flow-button"
>
{formatMessage('flowEditor.export')}
</Button>
)}
</Can>
<Can I="publish" a="Flow" passThrough>
{(allowed) => (
<Button

View File

@@ -12,6 +12,8 @@ import * as URLS from 'config/urls';
import useFormatMessage from 'hooks/useFormatMessage';
import useDuplicateFlow from 'hooks/useDuplicateFlow';
import useDeleteFlow from 'hooks/useDeleteFlow';
import useExportFlow from 'hooks/useExportFlow';
import useDownloadJsonAsFile from 'hooks/useDownloadJsonAsFile';
function ContextMenu(props) {
const { flowId, onClose, anchorEl, onDuplicateFlow, onDeleteFlow, appKey } =
@@ -20,7 +22,9 @@ function ContextMenu(props) {
const formatMessage = useFormatMessage();
const queryClient = useQueryClient();
const { mutateAsync: duplicateFlow } = useDuplicateFlow(flowId);
const { mutateAsync: deleteFlow } = useDeleteFlow();
const { mutateAsync: deleteFlow } = useDeleteFlow(flowId);
const { mutateAsync: exportFlow } = useExportFlow(flowId);
const downloadJsonAsFile = useDownloadJsonAsFile();
const onFlowDuplicate = React.useCallback(async () => {
await duplicateFlow();
@@ -51,7 +55,7 @@ function ContextMenu(props) {
]);
const onFlowDelete = React.useCallback(async () => {
await deleteFlow(flowId);
await deleteFlow();
if (appKey) {
await queryClient.invalidateQueries({
@@ -65,7 +69,30 @@ function ContextMenu(props) {
onDeleteFlow?.();
onClose();
}, [flowId, onClose, deleteFlow, queryClient, onDeleteFlow]);
}, [
deleteFlow,
appKey,
enqueueSnackbar,
formatMessage,
onDeleteFlow,
onClose,
queryClient,
]);
const onFlowExport = React.useCallback(async () => {
const flowExport = await exportFlow();
downloadJsonAsFile({
contents: flowExport.data,
name: flowExport.data.name,
});
enqueueSnackbar(formatMessage('flow.successfullyExported'), {
variant: 'success',
});
onClose();
}, [exportFlow, downloadJsonAsFile, enqueueSnackbar, formatMessage, onClose]);
return (
<Menu
@@ -90,6 +117,14 @@ function ContextMenu(props) {
)}
</Can>
<Can I="read" a="Flow" passThrough>
{(allowed) => (
<MenuItem disabled={!allowed} onClick={onFlowExport}>
{formatMessage('flow.export')}
</MenuItem>
)}
</Can>
<Can I="delete" a="Flow" passThrough>
{(allowed) => (
<MenuItem disabled={!allowed} onClick={onFlowDelete}>

View File

@@ -2,11 +2,11 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import api from 'helpers/api';
export default function useDeleteFlow() {
export default function useDeleteFlow(flowId) {
const queryClient = useQueryClient();
const query = useMutation({
mutationFn: async (flowId) => {
mutationFn: async () => {
const { data } = await api.delete(`/v1/flows/${flowId}`);
return data;

View File

@@ -0,0 +1,31 @@
import * as React from 'react';
import slugify from 'slugify';
export default function useDownloadJsonAsFile() {
const handleDownloadJsonAsFile = React.useCallback(
function handleDownloadJsonAsFile({ contents, name }) {
const stringifiedContents = JSON.stringify(contents, null, 2);
const slugifiedName = slugify(name, {
lower: true,
strict: true,
replacement: '-',
});
const fileBlob = new Blob([stringifiedContents], {
type: 'application/json',
});
const fileObjectUrl = URL.createObjectURL(fileBlob);
const temporaryDownloadLink = document.createElement('a');
temporaryDownloadLink.href = fileObjectUrl;
temporaryDownloadLink.download = slugifiedName;
temporaryDownloadLink.click();
},
[],
);
return handleDownloadJsonAsFile;
}

View File

@@ -0,0 +1,15 @@
import { useMutation } from '@tanstack/react-query';
import api from 'helpers/api';
export default function useExportFlow(flowId) {
const mutation = useMutation({
mutationFn: async () => {
const { data } = await api.post(`/v1/flows/${flowId}/export`);
return data;
},
});
return mutation;
}

View File

@@ -56,9 +56,11 @@
"flow.draft": "Draft",
"flow.successfullyDeleted": "The flow and associated executions have been deleted.",
"flow.successfullyDuplicated": "The flow has been successfully duplicated.",
"flow.successfullyExported": "The flow export has been successfully generated.",
"flowEditor.publish": "PUBLISH",
"flowEditor.unpublish": "UNPUBLISH",
"flowEditor.publishedFlowCannotBeUpdated": "To edit this flow, you must first unpublish it.",
"flowEditor.export": "EXPORT",
"flowEditor.noTestDataTitle": "We couldn't find matching data",
"flowEditor.noTestDataMessage": "Create a sample in the associated service and test the step again.",
"flowEditor.testAndContinue": "Test & Continue",
@@ -70,6 +72,7 @@
"flowEditor.triggerEvent": "Trigger event",
"flowEditor.actionEvent": "Action event",
"flowEditor.instantTriggerType": "Instant",
"flowEditor.flowSuccessfullyExported": "The flow export has been successfully generated.",
"filterConditions.onlyContinueIf": "Only continue if…",
"filterConditions.orContinueIf": "OR continue if…",
"chooseConnectionSubstep.continue": "Continue",
@@ -81,6 +84,7 @@
"flow.view": "View",
"flow.duplicate": "Duplicate",
"flow.delete": "Delete",
"flow.export": "Export",
"flowStep.triggerType": "Trigger",
"flowStep.actionType": "Action",
"flows.create": "Create flow",

View File

@@ -9638,6 +9638,11 @@ slate@^0.94.1:
is-plain-object "^5.0.0"
tiny-warning "^1.0.3"
slugify@^1.6.6:
version "1.6.6"
resolved "https://registry.yarnpkg.com/slugify/-/slugify-1.6.6.tgz#2d4ac0eacb47add6af9e04d3be79319cbcc7924b"
integrity sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==
sockjs@^0.3.24:
version "0.3.24"
resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce"