Compare commits

...

29 Commits

Author SHA1 Message Date
kasia.oczkowska
96c3c19a50 feat: introduce paths 2024-06-26 15:45:26 +01:00
Ali BARIN
abfd1116c7 Merge pull request #1941 from automatisch/AUT-1038
fix: persist value in ControlledCustomAutocomplete when it depends on other fields
2024-06-21 14:32:09 +02:00
kasia.oczkowska
017854955d fix: persist value in ControlledCustomAutocomplete when it depends on other fields 2024-06-21 10:37:39 +01:00
Ali BARIN
1405cddea1 Merge pull request #1940 from automatisch/AUT-1039
Revert "feat: persist parameters values in FlowSubstep (#1505)"
2024-06-20 15:57:47 +02:00
kasia.oczkowska
00dd3164c9 Revert "feat: persist parameters values in FlowSubstep (#1505)"
This reverts commit 017a881494.
2024-06-20 14:48:51 +01:00
Ali BARIN
d5cbc0f611 Merge pull request #1578 from automatisch/AUT-620
feat: improve UI display depending on user permissions
2024-06-20 12:04:31 +02:00
kasia.oczkowska
5d2e9ccc67 feat: improve UI display depending on user persmissions 2024-06-20 09:52:00 +00:00
kattoczko
017a881494 feat: persist parameters values in FlowSubstep (#1505)
* feat: persist parameters values in FlowSubstep

* feat: add missing import

* feat: use usePrevious hook
2024-06-20 11:13:45 +02:00
Alex Maslakov
52994970e6 fix(DynamicField): display long variables better
* Fix issue #1933

Problem with rendering in DynamicField component with long variables #1933

* Fix issue #1933 (2)

* Fix issue #1933 (3)
2024-06-20 09:55:08 +02:00
Ali BARIN
ebae629e5c Merge pull request #1931 from automatisch/AUT-1010
feat: improve nodes and edges state update
2024-06-19 16:01:24 +02:00
kasia.oczkowska
4d79220b0c refactor: fix spelling and wording errors 2024-06-14 12:39:42 +01:00
kasia.oczkowska
96fba7fbb8 feat: improve nodes and edges state update 2024-06-14 12:39:42 +01:00
Ali BARIN
e0d610071d Merge pull request #1917 from automatisch/AUT-1009
feat: hide react-flow attribution
2024-06-05 15:35:09 +02:00
kasia.oczkowska
ab0966c005 feat: hide react-flow attribution 2024-06-05 13:39:49 +01:00
Ali BARIN
751eb41e72 Merge pull request #1817 from automatisch/new-editor-feature-flag
feat: introduce feature flag for new flow editor
2024-06-04 12:45:00 +02:00
kasia.oczkowska
f08dc25711 feat: introduce style and behavior improvements 2024-06-04 07:18:18 +00:00
kasia.oczkowska
737eb31776 feat: introduce custom edges, auto layout improvements and node data updates 2024-06-04 07:17:38 +00:00
kasia.oczkowska
d6abf283bc feat: introduce automatic layout for new flow editor 2024-06-04 07:17:38 +00:00
kasia.oczkowska
bac4ab5aa4 feat: introduce feature flag for new flow editor 2024-06-04 07:17:38 +00:00
Ali BARIN
b5839390fd Merge pull request #1911 from automatisch/render-yaml-fix
fix(render.yaml): correct docker contexts
2024-06-03 11:21:31 +02:00
Ali BARIN
d19271dae1 fix(render.yaml): correct docker contexts 2024-06-03 10:23:28 +02:00
Ali BARIN
ef5a09314e Merge pull request #1907 from automatisch/fix-admin-apps-undefined-error
fix(AdminApplicationSettings): handle undefined appConfig object
2024-05-31 13:52:13 +02:00
Rıdvan Akca
ba52e298eb fix(AdminApplicationSettings): handle undefined appConfig object 2024-05-31 13:26:06 +02:00
Ali BARIN
b3c3998189 Merge pull request #1895 from automatisch/fix-appkey-error-in-flowrow
fix: remove unnecessary appKey in FlowRow and FlowContextMenu
2024-05-31 12:54:13 +02:00
Ömer Faruk Aydın
782f9b5c04 Merge pull request #1900 from automatisch/release/v0.12.0
release(v0.12.0): Update version to 0.12.0
2024-05-28 12:05:52 +02:00
Faruk AYDIN
3079d8c605 Update version to 0.12.0 2024-05-28 11:13:54 +02:00
Rıdvan Akca
c5202d7b3e fix: remove unnecessary appKey in FlowRow and FlowContextMenu 2024-05-24 14:11:22 +02:00
Ali BARIN
fbae83f4de Merge pull request #1874 from automatisch/make-value-column-text-in-datastore
fix(datastore): make value column text
2024-05-23 10:13:47 +02:00
Ali BARIN
1dc9646894 fix(datastore): make value column text 2024-05-09 20:31:47 +00:00
42 changed files with 1840 additions and 106 deletions

View File

@@ -52,7 +52,7 @@ const appConfig = {
isDev: appEnv === 'development',
isTest: appEnv === 'test',
isProd: appEnv === 'production',
version: '0.11.0',
version: '0.12.0',
postgresDatabase: process.env.POSTGRES_DATABASE || 'automatisch_development',
postgresSchema: process.env.POSTGRES_SCHEMA || 'public',
postgresPort: parseInt(process.env.POSTGRES_PORT || '5432'),

View File

@@ -10,7 +10,7 @@ describe('GET /api/v1/automatisch/version', () => {
const expectedPayload = {
data: {
version: '0.11.0',
version: '0.12.0',
},
meta: {
count: 1,

View File

@@ -0,0 +1,11 @@
export async function up(knex) {
return knex.schema.alterTable('datastore', (table) => {
table.text('value').alter();
});
}
export async function down(knex) {
return knex.schema.alterTable('datastore', (table) => {
table.string('value').alter();
});
}

View File

@@ -7,6 +7,7 @@
"@apollo/client": "^3.6.9",
"@casl/ability": "^6.5.0",
"@casl/react": "^3.1.0",
"@dagrejs/dagre": "^1.1.2",
"@emotion/react": "^11.4.1",
"@emotion/styled": "^11.3.0",
"@hookform/resolvers": "^2.8.8",
@@ -32,6 +33,7 @@
"react-router-dom": "^6.0.2",
"react-scripts": "5.0.0",
"react-window": "^1.8.9",
"reactflow": "^11.11.2",
"slate": "^0.94.1",
"slate-history": "^0.93.0",
"slate-react": "^0.94.2",

View File

@@ -36,7 +36,7 @@ function AdminApplicationSettings(props) {
const handleSubmit = async (values) => {
try {
if (!appConfig.data) {
if (!appConfig?.data) {
await createAppConfig({
variables: {
input: { key: props.appKey, ...values },
@@ -69,6 +69,7 @@ function AdminApplicationSettings(props) {
}),
[appConfig?.data],
);
return (
<Form
defaultValues={defaultValues}

View File

@@ -8,6 +8,7 @@ import * as URLS from 'config/urls';
import useFormatMessage from 'hooks/useFormatMessage';
import { ConnectionPropType } from 'propTypes/propTypes';
import { useQueryClient } from '@tanstack/react-query';
import Can from 'components/Can';
function ContextMenu(props) {
const {
@@ -44,34 +45,57 @@ function ContextMenu(props) {
hideBackdrop={false}
anchorEl={anchorEl}
>
<MenuItem
component={Link}
to={URLS.APP_FLOWS_FOR_CONNECTION(appKey, connection.id)}
onClick={createActionHandler({ type: 'viewFlows' })}
>
{formatMessage('connection.viewFlows')}
</MenuItem>
<MenuItem onClick={createActionHandler({ type: 'test' })}>
{formatMessage('connection.testConnection')}
</MenuItem>
<MenuItem
component={Link}
disabled={disableReconnection}
to={URLS.APP_RECONNECT_CONNECTION(
appKey,
connection.id,
connection.appAuthClientId,
<Can I="read" a="Flow" passThrough>
{(allowed) => (
<MenuItem
component={Link}
to={URLS.APP_FLOWS_FOR_CONNECTION(appKey, connection.id)}
onClick={createActionHandler({ type: 'viewFlows' })}
disabled={!allowed}
>
{formatMessage('connection.viewFlows')}
</MenuItem>
)}
onClick={createActionHandler({ type: 'reconnect' })}
>
{formatMessage('connection.reconnect')}
</MenuItem>
</Can>
<MenuItem onClick={createActionHandler({ type: 'delete' })}>
{formatMessage('connection.delete')}
</MenuItem>
<Can I="update" a="Connection" passThrough>
{(allowed) => (
<MenuItem
onClick={createActionHandler({ type: 'test' })}
disabled={!allowed}
>
{formatMessage('connection.testConnection')}
</MenuItem>
)}
</Can>
<Can I="create" a="Connection" passThrough>
{(allowed) => (
<MenuItem
component={Link}
disabled={!allowed || disableReconnection}
to={URLS.APP_RECONNECT_CONNECTION(
appKey,
connection.id,
connection.appAuthClientId,
)}
onClick={createActionHandler({ type: 'reconnect' })}
>
{formatMessage('connection.reconnect')}
</MenuItem>
)}
</Can>
<Can I="delete" a="Connection" passThrough>
{(allowed) => (
<MenuItem
onClick={createActionHandler({ type: 'delete' })}
disabled={!allowed}
>
{formatMessage('connection.delete')}
</MenuItem>
)}
</Can>
</Menu>
);
}

View File

@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import AppConnectionRow from 'components/AppConnectionRow';
import NoResultFound from 'components/NoResultFound';
import Can from 'components/Can';
import useFormatMessage from 'hooks/useFormatMessage';
import * as URLS from 'config/urls';
import useAppConnections from 'hooks/useAppConnections';
@@ -16,11 +17,15 @@ function AppConnections(props) {
if (!hasConnections) {
return (
<NoResultFound
to={URLS.APP_ADD_CONNECTION(appKey)}
text={formatMessage('app.noConnections')}
data-test="connections-no-results"
/>
<Can I="create" a="Connection" passThrough>
{(allowed) => (
<NoResultFound
text={formatMessage('app.noConnections')}
data-test="connections-no-results"
{...(allowed && { to: URLS.APP_ADD_CONNECTION(appKey) })}
/>
)}
</Can>
);
}

View File

@@ -5,6 +5,7 @@ import PaginationItem from '@mui/material/PaginationItem';
import * as URLS from 'config/urls';
import AppFlowRow from 'components/FlowRow';
import Can from 'components/Can';
import NoResultFound from 'components/NoResultFound';
import useFormatMessage from 'hooks/useFormatMessage';
import useConnectionFlows from 'hooks/useConnectionFlows';
@@ -36,11 +37,20 @@ function AppFlows(props) {
if (!hasFlows) {
return (
<NoResultFound
to={URLS.CREATE_FLOW_WITH_APP_AND_CONNECTION(appKey, connectionId)}
text={formatMessage('app.noFlows')}
data-test="flows-no-results"
/>
<Can I="create" a="Flow" passThrough>
{(allowed) => (
<NoResultFound
text={formatMessage('app.noFlows')}
data-test="flows-no-results"
{...(allowed && {
to: URLS.CREATE_FLOW_WITH_APP_AND_CONNECTION(
appKey,
connectionId
),
})}
/>
)}
</Can>
);
}

View File

@@ -165,6 +165,7 @@ function ChooseAppAndEventSubstep(props) {
value={getOption(appOptions, step.appKey) || null}
onChange={onAppChange}
data-test="choose-app-autocomplete"
componentsProps={{ popper: { className: 'nowheel' } }}
/>
{step.appKey && (
@@ -227,6 +228,7 @@ function ChooseAppAndEventSubstep(props) {
value={getOption(actionOrTriggerOptions, step.key) || null}
onChange={onEventChange}
data-test="choose-event-autocomplete"
componentsProps={{ popper: { className: 'nowheel' } }}
/>
</Box>
)}

View File

@@ -240,6 +240,7 @@ function ChooseConnectionSubstep(props) {
onChange={handleChange}
loading={isAppConnectionsLoading}
data-test="choose-connection-autocomplete"
componentsProps={{ popper: { className: 'nowheel' } }}
/>
<Button

View File

@@ -32,9 +32,11 @@ function ControlledAutocomplete(props) {
...autocompleteProps
} = props;
let dependsOnValues = [];
if (dependsOn?.length) {
dependsOnValues = watch(dependsOn);
}
React.useEffect(() => {
const hasDependencies = dependsOnValues.length;
const allDepsSatisfied = dependsOnValues.every(Boolean);
@@ -44,6 +46,7 @@ function ControlledAutocomplete(props) {
resetField(name);
}
}, dependsOnValues);
return (
<Controller
rules={{ required }}

View File

@@ -47,6 +47,7 @@ const CustomOptions = (props) => {
},
},
]}
className="nowheel"
>
<Paper elevation={5} sx={{ width: '100%' }}>
<Tabs

View File

@@ -61,6 +61,7 @@ function ControlledCustomAutocomplete(props) {
const [isSingleChoice, setSingleChoice] = React.useState(undefined);
const priorStepsWithExecutions = React.useContext(StepExecutionsContext);
const editorRef = React.useRef(null);
const mountedRef = React.useRef(false);
const renderElement = React.useCallback(
(props) => <Element {...props} disabled={disabled} />,
@@ -94,10 +95,14 @@ function ControlledCustomAutocomplete(props) {
}, []);
React.useEffect(() => {
const hasDependencies = dependsOnValues.length;
if (hasDependencies) {
// Reset the field when a dependent has been updated
resetEditor(editor);
if (mountedRef.current) {
const hasDependencies = dependsOnValues.length;
if (hasDependencies) {
// Reset the field when a dependent has been updated
resetEditor(editor);
}
} else {
mountedRef.current = true;
}
}, dependsOnValues);

View File

@@ -64,11 +64,19 @@ function DynamicField(props) {
<Stack
direction={{ xs: 'column', sm: 'row' }}
spacing={{ xs: 2 }}
sx={{ display: 'flex', flex: 1 }}
sx={{
display: 'flex',
flex: 1,
minWidth: 0,
}}
>
{fields.map((fieldSchema, fieldSchemaIndex) => (
<Box
sx={{ display: 'flex', flex: '1 0 0px' }}
sx={{
display: 'flex',
flex: '1 0 0px',
minWidth: 0,
}}
key={`field-${field.__id}-${fieldSchemaIndex}`}
>
<InputCreator

View File

@@ -8,6 +8,7 @@ import Tooltip from '@mui/material/Tooltip';
import IconButton from '@mui/material/IconButton';
import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew';
import Snackbar from '@mui/material/Snackbar';
import { ReactFlowProvider } from 'reactflow';
import { EditorProvider } from 'contexts/Editor';
import EditableTypography from 'components/EditableTypography';
@@ -20,6 +21,9 @@ import * as URLS from 'config/urls';
import { TopBar } from './style';
import useFlow from 'hooks/useFlow';
import { useQueryClient } from '@tanstack/react-query';
import EditorNew from 'components/EditorNew/EditorNew';
const useNewFlowEditor = process.env.REACT_APP_USE_NEW_FLOW_EDITOR === 'true';
export default function EditorLayout() {
const { flowId } = useParams();
@@ -131,15 +135,28 @@ export default function EditorLayout() {
</Button>
</Box>
</TopBar>
<Stack direction="column" height="100%">
<Container maxWidth="md">
<EditorProvider value={{ readOnly: !!flow?.active }}>
{!flow && !isFlowLoading && 'not found'}
{flow && <Editor flow={flow} />}
</EditorProvider>
</Container>
</Stack>
{useNewFlowEditor ? (
<Stack direction="column" height="100%" flexGrow={1}>
<Stack direction="column" flexGrow={1}>
<EditorProvider value={{ readOnly: !!flow?.active }}>
<ReactFlowProvider>
{!flow && !isFlowLoading && 'not found'}
{flow && <EditorNew flow={flow} />}
</ReactFlowProvider>
</EditorProvider>
</Stack>
</Stack>
) : (
<Stack direction="column" height="100%">
<Container maxWidth="md">
<EditorProvider value={{ readOnly: !!flow?.active }}>
{!flow && !isFlowLoading && 'not found'}
{flow && <Editor flow={flow} />}
</EditorProvider>
</Container>
</Stack>
)}
<Snackbar
data-test="flow-cannot-edit-info-snackbar"

View File

@@ -0,0 +1,69 @@
import { EdgeLabelRenderer, getStraightPath, BaseEdge } from 'reactflow';
import IconButton from '@mui/material/IconButton';
import AddIcon from '@mui/icons-material/Add';
import PropTypes from 'prop-types';
import { useContext } from 'react';
import { EdgesContext } from '../../EditorNew';
import { Tooltip } from '@mui/material';
export default function NodeEdge({
sourceX,
sourceY,
targetX,
targetY,
source,
data: { laidOut },
}) {
const { stepCreationInProgress, flowActive, onAddStep } =
useContext(EdgesContext);
const [edgePath, labelX, labelY] = getStraightPath({
sourceX,
sourceY,
targetX,
targetY,
});
const handleAddStep = () => {
onAddStep(source);
};
return (
<>
<BaseEdge path={edgePath} />
<EdgeLabelRenderer>
<Tooltip title="Add step">
<IconButton
onClick={handleAddStep}
color="primary"
sx={{
position: 'absolute',
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
pointerEvents: 'all',
backgroundColor: '#fafafa',
'&:hover': {
backgroundColor: '#f0f3fa',
},
// visibility: laidOut ? 'visible' : 'hidden',
}}
disabled={stepCreationInProgress || flowActive}
>
<AddIcon />
</IconButton>
</Tooltip>
</EdgeLabelRenderer>
</>
);
}
NodeEdge.propTypes = {
sourceX: PropTypes.number.isRequired,
sourceY: PropTypes.number.isRequired,
targetX: PropTypes.number.isRequired,
targetY: PropTypes.number.isRequired,
source: PropTypes.string.isRequired,
data: PropTypes.shape({
laidOut: PropTypes.bool,
}).isRequired,
};

View File

@@ -0,0 +1,95 @@
import { EdgeLabelRenderer, getStraightPath, BaseEdge } from 'reactflow';
import IconButton from '@mui/material/IconButton';
import AddIcon from '@mui/icons-material/Add';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import PropTypes from 'prop-types';
import { useContext, useState } from 'react';
import { EdgesContext } from '../../EditorNew';
import { Tooltip } from '@mui/material';
export default function NodeOrPathsEdge({
sourceX,
sourceY,
targetX,
targetY,
source,
data: { laidOut },
}) {
const [anchorEl, setAnchorEl] = useState(null);
const open = Boolean(anchorEl);
const handleClick = (event) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const { stepCreationInProgress, flowActive, onAddStep, onAddPaths } =
useContext(EdgesContext);
const [edgePath, labelX, labelY] = getStraightPath({
sourceX,
sourceY,
targetX,
targetY,
});
const handleAddStep = () => {
onAddStep(source);
handleClose();
};
const handleAddPaths = () => {
onAddPaths(source);
handleClose();
};
return (
<>
<BaseEdge path={edgePath} />
<EdgeLabelRenderer>
<Tooltip title="Add step or paths">
<IconButton
onClick={handleClick}
color="primary"
sx={{
position: 'absolute',
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
pointerEvents: 'all',
backgroundColor: '#fafafa',
'&:hover': {
backgroundColor: '#f0f3fa',
},
// visibility: laidOut ? 'visible' : 'hidden',
}}
disabled={stepCreationInProgress || flowActive}
>
<AddIcon />
</IconButton>
</Tooltip>
<Menu
anchorEl={anchorEl}
open={open}
onClose={handleClose}
anchorOrigin={{ horizontal: 'right', vertical: 'top' }}
>
<MenuItem onClick={handleAddStep}>Step</MenuItem>
<MenuItem onClick={handleAddPaths}>Paths</MenuItem>
</Menu>
</EdgeLabelRenderer>
</>
);
}
NodeOrPathsEdge.propTypes = {
sourceX: PropTypes.number.isRequired,
sourceY: PropTypes.number.isRequired,
targetX: PropTypes.number.isRequired,
targetY: PropTypes.number.isRequired,
source: PropTypes.string.isRequired,
data: PropTypes.shape({
laidOut: PropTypes.bool,
}).isRequired,
};

View File

@@ -0,0 +1,69 @@
import { EdgeLabelRenderer, getStraightPath, BaseEdge } from 'reactflow';
import IconButton from '@mui/material/IconButton';
import AddIcon from '@mui/icons-material/Add';
import PropTypes from 'prop-types';
import { useContext } from 'react';
import { EdgesContext } from '../../EditorNew';
import { Tooltip } from '@mui/material';
export default function PathsEdge({
sourceX,
sourceY,
targetX,
targetY,
source,
data: { laidOut },
}) {
const { stepCreationInProgress, flowActive, onAddPath } =
useContext(EdgesContext);
const [edgePath, labelX, labelY] = getStraightPath({
sourceX,
sourceY,
targetX,
targetY,
});
const handleAddPath = () => {
onAddPath(source);
};
return (
<>
<BaseEdge path={edgePath} />
<EdgeLabelRenderer>
<Tooltip title="Add path">
<IconButton
onClick={handleAddPath}
color="primary"
sx={{
position: 'absolute',
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
pointerEvents: 'all',
backgroundColor: '#fafafa',
'&:hover': {
backgroundColor: '#f0f3fa',
},
// visibility: laidOut ? 'visible' : 'hidden',
}}
disabled={stepCreationInProgress || flowActive}
>
<AddIcon />
</IconButton>
</Tooltip>
</EdgeLabelRenderer>
</>
);
}
PathsEdge.propTypes = {
sourceX: PropTypes.number.isRequired,
sourceY: PropTypes.number.isRequired,
targetX: PropTypes.number.isRequired,
targetY: PropTypes.number.isRequired,
source: PropTypes.string.isRequired,
data: PropTypes.shape({
laidOut: PropTypes.bool,
}).isRequired,
};

View File

@@ -0,0 +1,184 @@
import { useEffect, useCallback, createContext, useRef } from 'react';
// import { useMutation } from '@apollo/client';
// import { useQueryClient } from '@tanstack/react-query';
import { FlowPropType } from 'propTypes/propTypes';
import ReactFlow, { useNodesState, useEdgesState } from 'reactflow';
import 'reactflow/dist/style.css';
// import { UPDATE_STEP } from 'graphql/mutations/update-step';
// import { CREATE_STEP } from 'graphql/mutations/create-step';
import { useAutoLayout } from './useAutoLayout';
import NodeOrPathsEdge from './Edges/NodeOrPathsEdge/NodeOrPathsEdge';
import FlowStepNode from './Nodes/FlowStepNode/FlowStepNode';
import InvisibleNode from './Nodes/InvisibleNode/InvisibleNode';
import PathsNode from './Nodes/PathsNode/PathsNode';
import { EditorWrapper } from './style';
import { generateEdges, generateNodes, updatedCollapsedNodes } from './utils';
import { EDGE_TYPES, NODE_TYPES } from './constants';
import { useFlow } from './temp/useFlow';
import PathNode from './Nodes/PathNode/PathNode';
import PathsEdge from './Edges/PathsEdge/PathsEdge';
import NodeEdge from './Edges/NodeEdge/NodeEdge';
export const EdgesContext = createContext();
export const NodesContext = createContext();
const nodeTypes = {
[NODE_TYPES.FLOW_STEP]: FlowStepNode,
[NODE_TYPES.INVISIBLE]: InvisibleNode,
[NODE_TYPES.PATHS]: PathsNode,
[NODE_TYPES.PATH]: PathNode,
};
const edgeTypes = {
[EDGE_TYPES.ADD_NODE_OR_PATHS_EDGE]: NodeOrPathsEdge,
[EDGE_TYPES.ADD_PATH_EDGE]: PathsEdge,
[EDGE_TYPES.ADD_NODE_EDGE]: NodeEdge,
};
const EditorNew = () =>
// { flow }
{
const { flow, createStep, createPaths, createPath } = useFlow();
// const [updateStep] = useMutation(UPDATE_STEP);
// const queryClient = useQueryClient();
// const [createStep, { loading: stepCreationInProgress }] =
// useMutation(CREATE_STEP);
const stepCreationInProgress = false;
const [nodes, setNodes, onNodesChange] = useNodesState(
generateNodes({ steps: flow.steps }),
);
const [edges, setEdges, onEdgesChange] = useEdgesState(
generateEdges({ steps: flow.steps }),
);
useAutoLayout();
const createdStepIdRef = useRef(null);
const openNextStep = useCallback(
(currentStepId) => {
setNodes((nodes) => {
const currentStepIndex = nodes.findIndex(
(node) => node.id === currentStepId,
);
if (currentStepIndex >= 0) {
const nextStep = nodes[currentStepIndex + 1];
return updatedCollapsedNodes(nodes, nextStep.id);
}
return nodes;
});
},
[setNodes],
);
const onStepClose = useCallback(() => {
setNodes((nodes) => updatedCollapsedNodes(nodes));
}, [setNodes]);
const onStepOpen = useCallback(
(stepId) => {
setNodes((nodes) => updatedCollapsedNodes(nodes, stepId));
},
[setNodes],
);
const onStepChange = useCallback(
async (step) => {
// const mutationInput = {
// id: step.id,
// key: step.key,
// parameters: step.parameters,
// connection: {
// id: step.connection?.id,
// },
// flow: {
// id: flow.id,
// },
// };
// if (step.appKey) {
// mutationInput.appKey = step.appKey;
// }
// const updated = await updateStep({
// variables: { input: mutationInput },
// });
// await queryClient.invalidateQueries({
// queryKey: ['steps', step.id, 'connection'],
// });
// await queryClient.invalidateQueries({ queryKey: ['flows', flow.id] });
},
// [flow.id, updateStep, queryClient],
);
const onAddStep = async (previousStepId) => {
const createdStepId = createStep(flow, previousStepId);
createdStepIdRef.current = createdStepId;
};
console.log({ flow });
useEffect(() => {
// if (flow.steps.length + 1 !== nodes.length) {
setNodes((nodes) =>
generateNodes({
prevNodes: nodes,
steps: flow.steps,
createdStepId: createdStepIdRef.current,
}),
);
setEdges((edges) =>
generateEdges({ prevEdges: edges, steps: flow.steps }),
);
if (createdStepIdRef.current) {
createdStepIdRef.current = null;
}
// }
}, [flow.steps]);
return (
<NodesContext.Provider
value={{
openNextStep,
onStepOpen,
onStepClose,
onStepChange,
flowId: flow.id,
steps: flow.steps,
}}
>
<EdgesContext.Provider
value={{
stepCreationInProgress,
onAddStep,
onAddPaths: createPaths,
onAddPath: createPath,
flowActive: flow.active,
}}
>
<EditorWrapper direction="column">
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
fitView
maxZoom={1}
minZoom={0.001}
proOptions={{ hideAttribution: true }}
/>
</EditorWrapper>
</EdgesContext.Provider>
</NodesContext.Provider>
);
};
EditorNew.propTypes = {
flow: FlowPropType.isRequired,
};
export default EditorNew;

View File

@@ -0,0 +1,69 @@
import { Handle, Position } from 'reactflow';
import PropTypes from 'prop-types';
import FlowStep from 'components/FlowStep';
import { NodeWrapper, NodeInnerWrapper } from './style.js';
import { useContext } from 'react';
import { NodesContext } from '../../EditorNew.jsx';
import { findStepByStepId } from 'components/EditorNew/utils.js';
function FlowStepNode({ data: { collapsed, laidOut }, id }) {
const { openNextStep, onStepOpen, onStepClose, onStepChange, flowId, steps } =
useContext(NodesContext);
const step = findStepByStepId({ steps }, id);
return (
// <NodeWrapper
// sx={{
// visibility: laidOut ? 'visible' : 'hidden',
// }}
// >
<NodeInnerWrapper
sx={
{
// visibility: laidOut ? 'visible' : 'hidden',
}
}
id="flowStepId"
className="nodrag"
>
<Handle
type="target"
position={Position.Top}
isConnectable={false}
style={{ visibility: 'hidden' }}
/>
{step && (
<FlowStep
step={step}
collapsed={collapsed}
onOpen={() => onStepOpen(step.id)}
onClose={onStepClose}
onChange={onStepChange}
flowId={flowId}
onContinue={() => openNextStep(step.id)}
collapseAnimation={false}
/>
)}
<Handle
type="source"
position={Position.Bottom}
isConnectable={false}
style={{ visibility: 'hidden' }}
/>
</NodeInnerWrapper>
// </NodeWrapper>
);
}
FlowStepNode.propTypes = {
id: PropTypes.string,
data: PropTypes.shape({
collapsed: PropTypes.bool.isRequired,
laidOut: PropTypes.bool.isRequired,
}).isRequired,
};
export default FlowStepNode;

View File

@@ -0,0 +1,14 @@
import { styled } from '@mui/material/styles';
import { Box } from '@mui/material';
export const NodeWrapper = styled(Box)(({ theme }) => ({
width: '100vw',
display: 'flex',
justifyContent: 'center',
padding: theme.spacing(0, 2.5),
}));
export const NodeInnerWrapper = styled(Box)(({ theme }) => ({
width: 900,
flex: 1,
}));

View File

@@ -0,0 +1,19 @@
import { Handle, Position } from 'reactflow';
import { Box } from '@mui/material';
// This node is used for adding an edge with add node button after the last flow step node
function InvisibleNode() {
return (
<Box
maxWidth={900}
width="100vw"
className="nodrag"
sx={{ visibility: 'hidden' }}
>
<Handle type="target" position={Position.Top} isConnectable={false} />
Invisible node
</Box>
);
}
export default InvisibleNode;

View File

@@ -0,0 +1,98 @@
import PropTypes from 'prop-types';
import { Handle, Position } from 'reactflow';
import { Box, Stack, Typography } from '@mui/material';
import { useRef, useState } from 'react';
import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import IconButton from '@mui/material/IconButton';
import { Wrapper } from './style';
/* TODO
- add delete
- add rename
- add translations
- add collapsing?
*/
function PathNode({ data: { laidOut } }) {
const [anchorEl, setAnchorEl] = useState(null);
const contextButtonRef = useRef(null);
const onContextMenuClose = (event) => {
event.stopPropagation();
setAnchorEl(null);
};
const onContextMenuClick = (event) => {
event.stopPropagation();
setAnchorEl(contextButtonRef.current);
};
const deletePath = () => {
setAnchorEl(null);
onContextMenuClose();
};
return (
<>
<Box
className="nodrag"
sx={
{
// visibility: laidOut ? 'visible' : 'hidden',
}
}
>
<Handle
type="target"
position={Position.Top}
isConnectable={false}
style={{ visibility: 'hidden' }}
/>
<Wrapper>
<Stack
justifyContent="space-between"
alignItems="center"
direction="row"
>
<Typography sx={{ pr: 2 }}>Path</Typography>
<IconButton
color="primary"
onClick={onContextMenuClick}
ref={contextButtonRef}
>
<MoreHorizIcon />
</IconButton>
</Stack>
</Wrapper>
<Handle
type="source"
position={Position.Bottom}
isConnectable={false}
style={{ visibility: 'hidden' }}
/>
</Box>
{anchorEl && (
<Menu
open={true}
onClose={onContextMenuClose}
hideBackdrop={false}
anchorEl={anchorEl}
>
<MenuItem onClick={deletePath}>Delete</MenuItem>
</Menu>
)}
</>
);
}
PathNode.propTypes = {
data: PropTypes.shape({
laidOut: PropTypes.bool,
}).isRequired,
};
export default PathNode;

View File

@@ -0,0 +1,8 @@
import { styled } from '@mui/material/styles';
import Box from '@mui/material/Box';
export const Wrapper = styled(Box)(({ theme }) => ({
padding: theme.spacing(1, 2),
backgroundColor: '#0059f714',
borderRadius: 20,
}));

View File

@@ -0,0 +1,108 @@
import PropTypes from 'prop-types';
import { Handle, Position } from 'reactflow';
import { Avatar, Box, Stack, Typography } from '@mui/material';
import { useRef, useState } from 'react';
import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import CallSplitIcon from '@mui/icons-material/CallSplit';
import IconButton from '@mui/material/IconButton';
import { Wrapper } from './style';
/* TODO
- add delete
- add rename
- add translations
- add collapsing?
*/
function PathsNode({ data: { laidOut } }) {
const [anchorEl, setAnchorEl] = useState(null);
const contextButtonRef = useRef(null);
const onContextMenuClose = (event) => {
event.stopPropagation();
setAnchorEl(null);
};
const onContextMenuClick = (event) => {
event.stopPropagation();
setAnchorEl(contextButtonRef.current);
};
const deletePaths = () => {
setAnchorEl(null);
onContextMenuClose();
};
return (
<>
<Box
width={900}
className="nodrag"
sx={
{
// visibility: laidOut ? 'visible' : 'hidden',
}
}
>
<Handle
type="target"
position={Position.Top}
isConnectable={false}
style={{ visibility: 'hidden' }}
/>
<Wrapper>
<Stack justifyContent="space-between" direction="row">
<Stack direction="row" alignItems="center" spacing={2}>
<Avatar
sx={{ display: 'flex', width: 50, height: 50 }}
variant="square"
>
<CallSplitIcon
fontSize="large"
sx={{ transform: 'rotate(180deg)' }}
/>
</Avatar>
{/* TODO name from path data */}
<Typography>Paths</Typography>
</Stack>
<IconButton
color="primary"
onClick={onContextMenuClick}
ref={contextButtonRef}
>
<MoreHorizIcon />
</IconButton>
</Stack>
</Wrapper>
<Handle
type="source"
position={Position.Bottom}
isConnectable={false}
style={{ visibility: 'hidden' }}
/>
</Box>
{anchorEl && (
<Menu
open={true}
onClose={onContextMenuClose}
hideBackdrop={false}
anchorEl={anchorEl}
>
<MenuItem onClick={deletePaths}>Delete</MenuItem>
</Menu>
)}
</>
);
}
PathsNode.propTypes = {
data: PropTypes.shape({
laidOut: PropTypes.bool,
}).isRequired,
};
export default PathsNode;

View File

@@ -0,0 +1,7 @@
import { styled } from '@mui/material/styles';
import Card from '@mui/material/Card';
export const Wrapper = styled(Card)`
width: 100%;
padding: ${({ theme }) => theme.spacing(2)};
`;

View File

@@ -0,0 +1,14 @@
export const INVISIBLE_NODE_ID = 'invisible-node';
export const NODE_TYPES = {
FLOW_STEP: 'flowStep',
INVISIBLE: 'invisible',
PATHS: 'parallelPaths',
PATH: 'path',
};
export const EDGE_TYPES = {
ADD_NODE_OR_PATHS_EDGE: 'addNodeOrPathsEdge',
ADD_PATH_EDGE: 'addPathEdge',
ADD_NODE_EDGE: 'addNodeEdge',
};

View File

@@ -0,0 +1,13 @@
import { Stack } from '@mui/material';
import { styled } from '@mui/material/styles';
export const EditorWrapper = styled(Stack)(({ theme }) => ({
flexGrow: 1,
'& > div': {
flexGrow: 1,
},
// '& .react-flow__pane, & .react-flow__node': {
// cursor: 'auto !important',
// },
}));

View File

@@ -0,0 +1,96 @@
import { useState } from 'react';
import { v4 as uuidv4 } from 'uuid';
import { insertStep } from '../utils';
const initFlow = {
id: '7c55e6ce-a84a-46e3-ba31-211ec7b5c2cb',
name: 'Name your flow',
active: false,
status: 'draft',
createdAt: 1718264916266,
updatedAt: 1718264916266,
steps: [
{
id: '82ce34ab-7aab-4e6c-9f62-db5104aa81c6',
type: 'trigger',
key: null,
appKey: null,
iconUrl: null,
webhookUrl: 'http://localhost:3000/null',
status: 'incomplete',
position: 1,
parameters: {},
},
{
id: '41c60527-eb4f-4f2d-93ec-2fd37e336909',
type: 'action',
key: null,
appKey: null,
iconUrl: null,
webhookUrl: 'http://localhost:3000/null',
status: 'incomplete',
position: 2,
parameters: {},
},
],
};
const generateStep = () => {
return {
id: uuidv4(),
type: 'action',
key: null,
appKey: null,
parameters: {},
iconUrl: null,
webhookUrl: 'http://localhost:3000/null',
status: 'incomplete',
connection: null,
position: null,
};
};
const generatePath = (steps) => {
return {
id: uuidv4(),
type: 'path',
steps: steps?.length > 0 ? steps : [generateStep()],
};
};
export const generatePaths = (steps) => {
return {
id: uuidv4(),
type: 'parallelPaths',
steps: [generatePath(steps), generatePath()],
};
};
export const useFlow = () => {
const [flow, setFlow] = useState(initFlow);
const createStep = (flow, previousStepId) => {
const newStep = generateStep();
const newFlow = insertStep(flow, previousStepId, newStep);
setFlow(newFlow);
return newStep.id;
};
const createPaths = (previousStepId) => {
const newFlow = insertStep(flow, previousStepId, generatePaths());
setFlow(newFlow);
};
const createPath = (previousStepId) => {
const newFlow = insertStep(flow, previousStepId, generatePath());
setFlow(newFlow);
};
return {
flow,
createStep,
createPaths,
createPath,
};
};

View File

@@ -0,0 +1,71 @@
import { useCallback, useEffect } from 'react';
import Dagre from '@dagrejs/dagre';
import { usePrevious } from 'hooks/usePrevious';
import { isEqual } from 'lodash';
import { useNodesInitialized, useNodes, useReactFlow } from 'reactflow';
const getLaidOutElements = (nodes, edges) => {
const graph = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
graph.setGraph({
rankdir: 'TB',
marginy: 60,
ranksep: 64,
});
edges.forEach((edge) => graph.setEdge(edge.source, edge.target));
nodes.forEach((node) => graph.setNode(node.id, node));
Dagre.layout(graph);
return {
nodes: nodes.map((node) => {
const { x, y, width, height } = graph.node(node.id);
return {
...node,
position: { x: x - width / 2, y: y - height / 2 },
};
}),
edges,
};
};
export const useAutoLayout = () => {
const nodes = useNodes();
const prevNodes = usePrevious(nodes);
const nodesInitialized = useNodesInitialized();
const { getEdges, setNodes, setEdges, fitView } = useReactFlow();
const onLayout = useCallback(
(nodes, edges) => {
const laidOutElements = getLaidOutElements(nodes, edges);
setNodes([
...laidOutElements.nodes.map((node) => ({
...node,
data: { ...node.data, laidOut: true },
})),
]);
setEdges([
...laidOutElements.edges.map((edge) => ({
...edge,
data: { ...edge.data, laidOut: true },
})),
]);
},
[setEdges, setNodes],
);
useEffect(() => {
const shouldAutoLayout =
nodesInitialized &&
!isEqual(
nodes.map(({ width, height }) => ({ width, height })),
prevNodes.map(({ width, height }) => ({ width, height })),
);
fitView();
if (shouldAutoLayout) {
onLayout(nodes, getEdges());
}
}, [nodes]);
};

View File

@@ -0,0 +1,13 @@
import { useEffect } from 'react';
import { useViewport, useReactFlow } from 'reactflow';
export const useScrollBoundaries = () => {
const { setViewport } = useReactFlow();
const { x, y, zoom } = useViewport();
useEffect(() => {
if (y > 0) {
setViewport({ x, y: 0, zoom });
}
}, [y]);
};

View File

@@ -0,0 +1,245 @@
import cloneDeep from 'lodash/cloneDeep.js';
import { EDGE_TYPES, INVISIBLE_NODE_ID, NODE_TYPES } from './constants';
export const generateEdgeId = (sourceId, targetId) =>
`${sourceId}--${targetId}`;
export const updatedCollapsedNodes = (nodes, openStepId) => {
return nodes.map((node) => {
if (node.type !== NODE_TYPES.FLOW_STEP) {
return node;
}
const collapsed = node.id !== openStepId;
return {
...node,
zIndex: collapsed ? 0 : 1,
data: { ...node.data, collapsed },
};
});
};
export const generateNodes = ({ steps, prevNodes, createdStepId }) => {
const newNodes = steps.map((step, index) => {
const collapsed = index !== 0;
const prevNode = prevNodes?.find(({ id }) => id === step.id);
let newNode;
let childSteps = [];
switch (step.type) {
case 'trigger':
case 'action': {
if (prevNode) {
newNode = {
...prevNode,
zIndex: createdStepId ? 0 : prevNode?.zIndex || 0,
data: {
...prevNode.data,
collapsed: createdStepId
? true
: prevNode?.data?.collapsed || true,
},
};
} else {
newNode = {
id: step.id,
type: NODE_TYPES.FLOW_STEP,
position: {
x: 0,
y: 0,
},
zIndex: collapsed ? 0 : 1,
data: {
collapsed,
laidOut: false,
},
};
}
break;
}
case 'parallelPaths': {
if (prevNode) {
newNode = prevNode;
} else {
newNode = {
id: step.id,
type: NODE_TYPES.PATHS,
position: {
x: 0,
y: 0,
},
data: {
laidOut: false,
},
};
}
break;
}
case 'path': {
if (prevNode) {
newNode = prevNode;
} else {
newNode = {
id: step.id,
type: NODE_TYPES.PATH,
position: {
x: 0,
y: 0,
},
data: {
laidOut: false,
},
};
}
break;
}
default:
}
if (step?.steps?.length > 0) {
childSteps = generateNodes({
steps: step.steps,
prevNodes,
createdStepId,
});
}
return [newNode, ...childSteps];
});
return newNodes.flat(Infinity);
};
export const generateEdges = ({ steps }) => {
const newEdges = steps.map((step, index) => {
switch (step.type) {
case 'parallelPaths': {
const edges = step.steps.map((childStep) => {
const sourceId = step.id;
const targetId = childStep.id;
const newEdge = {
id: generateEdgeId(sourceId, targetId),
source: sourceId,
target: targetId,
type: EDGE_TYPES.ADD_PATH_EDGE,
data: {
laidOut: false,
},
};
return newEdge;
});
const childEdges = generateEdges({ steps: step.steps });
return [...edges, ...childEdges];
}
case 'path': {
console.log({ step });
const sourceId = step.id;
const targetId = step.steps?.[0]?.id;
if (targetId) {
const newEdge = {
id: generateEdgeId(sourceId, targetId),
source: sourceId,
target: targetId,
type: EDGE_TYPES.ADD_NODE_EDGE,
data: {
laidOut: false,
},
};
const childEdges = generateEdges({ steps: step.steps });
return [newEdge, ...childEdges];
}
return null;
}
default: {
const sourceId = step.id;
const targetId = steps[index + 1]?.id;
if (targetId) {
return {
id: generateEdgeId(sourceId, targetId),
source: sourceId,
target: targetId,
type: EDGE_TYPES.ADD_NODE_OR_PATHS_EDGE,
data: {
laidOut: false,
},
};
}
return null;
}
}
});
return newEdges.flat(Infinity).filter((edge) => !!edge);
};
export const findStepByStepId = (obj, id) => {
if (Array.isArray(obj.steps)) {
for (const step of obj.steps) {
if (step.id === id) {
return step;
}
const result = findStepByStepId(step, id);
if (result) {
return result;
}
}
}
return null;
};
export function insertStep(parentObj, id, newStep) {
function recursiveFindAndInsert(parentObj, id, newStep) {
if (parentObj.steps && Array.isArray(parentObj.steps)) {
for (let index = 0; index < parentObj.steps.length; index++) {
const step = parentObj.steps[index];
if (step.id === id) {
if (newStep.type === NODE_TYPES.PATHS) {
const stepsAfter = parentObj.steps.slice(
index + 1,
parentObj.steps.length,
);
parentObj.steps.splice(index + 1);
newStep.steps[0].steps = stepsAfter;
parentObj.steps.splice(index + 1, 0, newStep);
} else if (step.type === NODE_TYPES.PATHS) {
step.steps.push(newStep);
} else if (step.type === NODE_TYPES.PATH) {
step.steps.unshift(newStep);
} else {
const originalSteps = step.steps || [];
step.steps = [];
const newStepObject = {
...newStep,
steps: originalSteps,
};
parentObj.steps.splice(index + 1, 0, newStepObject);
}
return true;
}
const found = recursiveFindAndInsert(step, id, newStep);
if (found) {
return true;
}
}
}
return false;
}
// Clone the input object to avoid mutating the original
const newParentObj = cloneDeep(parentObj);
recursiveFindAndInsert(newParentObj, id, newStep);
return newParentObj;
}

View File

@@ -28,9 +28,12 @@ function ContextMenu(props) {
variables: { input: { id: flowId } },
});
await queryClient.invalidateQueries({
queryKey: ['apps', appKey, 'flows'],
});
if (appKey) {
await queryClient.invalidateQueries({
queryKey: ['apps', appKey, 'flows'],
});
}
enqueueSnackbar(formatMessage('flow.successfullyDuplicated'), {
variant: 'success',
SnackbarProps: {
@@ -56,9 +59,12 @@ function ContextMenu(props) {
},
});
await queryClient.invalidateQueries({
queryKey: ['apps', appKey, 'flows'],
});
if (appKey) {
await queryClient.invalidateQueries({
queryKey: ['apps', appKey, 'flows'],
});
}
enqueueSnackbar(formatMessage('flow.successfullyDeleted'), {
variant: 'success',
});
@@ -110,7 +116,7 @@ ContextMenu.propTypes = {
]).isRequired,
onDeleteFlow: PropTypes.func,
onDuplicateFlow: PropTypes.func,
appKey: PropTypes.string.isRequired,
appKey: PropTypes.string,
};
export default ContextMenu;

View File

@@ -38,20 +38,24 @@ function FlowRow(props) {
const contextButtonRef = React.useRef(null);
const [anchorEl, setAnchorEl] = React.useState(null);
const { flow, onDuplicateFlow, onDeleteFlow, appKey } = props;
const handleClose = () => {
setAnchorEl(null);
};
const onContextMenuClick = (event) => {
event.preventDefault();
event.stopPropagation();
event.nativeEvent.stopImmediatePropagation();
setAnchorEl(contextButtonRef.current);
};
const createdAt = DateTime.fromMillis(parseInt(flow.createdAt, 10));
const updatedAt = DateTime.fromMillis(parseInt(flow.updatedAt, 10));
const isUpdated = updatedAt > createdAt;
const relativeCreatedAt = createdAt.toRelative();
const relativeUpdatedAt = updatedAt.toRelative();
return (
<>
<Card sx={{ mb: 1 }} data-test="flow-row">
@@ -127,7 +131,7 @@ FlowRow.propTypes = {
flow: FlowPropType.isRequired,
onDeleteFlow: PropTypes.func,
onDuplicateFlow: PropTypes.func,
appKey: PropTypes.string.isRequired,
appKey: PropTypes.string,
};
export default FlowRow;

View File

@@ -105,7 +105,7 @@ function generateValidationSchema(substeps) {
}
function FlowStep(props) {
const { collapsed, onChange, onContinue, flowId } = props;
const { collapsed, onChange, onContinue, flowId, collapseAnimation } = props;
const editorContext = React.useContext(EditorContext);
const contextButtonRef = React.useRef(null);
const step = props.step;
@@ -259,7 +259,11 @@ function FlowStep(props) {
</Stack>
</Header>
<Collapse in={!collapsed} unmountOnExit>
<Collapse
in={!collapsed}
unmountOnExit
{...(!collapseAnimation ? { timeout: 0 } : {})}
>
<Content>
<List>
<StepExecutionsProvider value={stepWithTestExecutionsData}>
@@ -360,11 +364,15 @@ function FlowStep(props) {
FlowStep.propTypes = {
collapsed: PropTypes.bool,
step: StepPropType.isRequired,
index: PropTypes.number,
onOpen: PropTypes.func,
onClose: PropTypes.func,
onChange: PropTypes.func.isRequired,
onContinue: PropTypes.func,
collapseAnimation: PropTypes.bool,
};
FlowStep.defaultProps = {
collapseAnimation: true,
};
export default FlowStep;

View File

@@ -80,6 +80,7 @@ export default function InputCreator(props) {
disabled={disabled}
showOptionValue={showOptionValue}
shouldUnregister={shouldUnregister}
componentsProps={{ popper: { className: 'nowheel' } }}
/>
)}

View File

@@ -17,6 +17,7 @@ import { StepExecutionsContext } from 'contexts/StepExecutions';
import Popper from './Popper';
import { processStepWithExecutions } from './data';
import { ChildrenWrapper, FakeInput, InputLabelWrapper } from './style';
const PowerInput = (props) => {
const { control } = useFormContext();
const {
@@ -31,33 +32,41 @@ const PowerInput = (props) => {
} = props;
const priorStepsWithExecutions = React.useContext(StepExecutionsContext);
const editorRef = React.useRef(null);
const renderElement = React.useCallback(
(props) => <Element {...props} />,
[],
);
const [editor] = React.useState(() => customizeEditor(createEditor()));
const [showVariableSuggestions, setShowVariableSuggestions] =
React.useState(false);
const disappearSuggestionsOnShift = (event) => {
if (event.code === 'Tab') {
setShowVariableSuggestions(false);
}
};
const stepsWithVariables = React.useMemo(() => {
return processStepWithExecutions(priorStepsWithExecutions);
}, [priorStepsWithExecutions]);
const handleBlur = React.useCallback(
(value) => {
onBlur?.(value);
},
[onBlur],
);
const handleVariableSuggestionClick = React.useCallback(
(variable) => {
insertVariable(editor, variable, stepsWithVariables);
},
[stepsWithVariables],
);
return (
<Controller
rules={{ required }}
@@ -127,6 +136,7 @@ const PowerInput = (props) => {
anchorEl={editorRef.current}
data={stepsWithVariables}
onSuggestionClick={handleVariableSuggestionClick}
className="nowheel"
/>
<FormHelperText variant="outlined">{description}</FormHelperText>

View File

@@ -0,0 +1,9 @@
import { useEffect, useRef } from "react";
export const usePrevious = (value) => {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
};

View File

@@ -30,6 +30,7 @@ import AppIcon from 'components/AppIcon';
import Container from 'components/Container';
import PageTitle from 'components/PageTitle';
import useApp from 'hooks/useApp';
import Can from 'components/Can';
const ReconnectConnection = (props) => {
const { application, onClose } = props;
@@ -92,7 +93,7 @@ export default function Application() {
}
return options;
}, [appKey, appConfig?.data, currentUserAbility]);
}, [appKey, appConfig?.data, currentUserAbility, formatMessage]);
if (loading) return null;
@@ -118,37 +119,46 @@ export default function Application() {
<Route
path={`${URLS.FLOWS}/*`}
element={
<ConditionalIconButton
type="submit"
variant="contained"
color="primary"
size="large"
component={Link}
to={URLS.CREATE_FLOW_WITH_APP_AND_CONNECTION(
appKey,
connectionId,
<Can I="create" a="Flow" passThrough>
{(allowed) => (
<ConditionalIconButton
type="submit"
variant="contained"
color="primary"
size="large"
component={Link}
to={URLS.CREATE_FLOW_WITH_APP_AND_CONNECTION(
appKey,
connectionId,
)}
fullWidth
icon={<AddIcon />}
disabled={!allowed}
>
{formatMessage('app.createFlow')}
</ConditionalIconButton>
)}
fullWidth
icon={<AddIcon />}
disabled={!currentUserAbility.can('create', 'Flow')}
>
{formatMessage('app.createFlow')}
</ConditionalIconButton>
</Can>
}
/>
<Route
path={`${URLS.CONNECTIONS}/*`}
element={
<SplitButton
disabled={
(appConfig?.data &&
!appConfig?.data?.canConnect &&
!appConfig?.data?.canCustomConnect) ||
connectionOptions.every(({ disabled }) => disabled)
}
options={connectionOptions}
/>
<Can I="create" a="Connection" passThrough>
{(allowed) => (
<SplitButton
disabled={
!allowed ||
(appConfig?.data &&
!appConfig?.data?.canConnect &&
!appConfig?.data?.canCustomConnect) ||
connectionOptions.every(({ disabled }) => disabled)
}
options={connectionOptions}
/>
)}
</Can>
}
/>
</Routes>
@@ -169,17 +179,20 @@ export default function Application() {
label={formatMessage('app.connections')}
to={URLS.APP_CONNECTIONS(appKey)}
value={URLS.APP_CONNECTIONS_PATTERN}
disabled={!app.supportsConnections}
disabled={
!currentUserAbility.can('read', 'Connection') ||
!app.supportsConnections
}
component={Link}
data-test="connections-tab"
/>
<Tab
label={formatMessage('app.flows')}
to={URLS.APP_FLOWS(appKey)}
value={URLS.APP_FLOWS_PATTERN}
component={Link}
data-test="flows-tab"
disabled={!currentUserAbility.can('read', 'Flow')}
/>
</Tabs>
</Box>
@@ -187,14 +200,20 @@ export default function Application() {
<Routes>
<Route
path={`${URLS.FLOWS}/*`}
element={<AppFlows appKey={appKey} />}
element={
<Can I="read" a="Flow">
<AppFlows appKey={appKey} />
</Can>
}
/>
<Route
path={`${URLS.CONNECTIONS}/*`}
element={<AppConnections appKey={appKey} />}
element={
<Can I="read" a="Connection">
<AppConnections appKey={appKey} />
</Can>
}
/>
<Route
path="/"
element={
@@ -218,17 +237,24 @@ export default function Application() {
<Route
path="/connections/add"
element={
<AddAppConnection onClose={goToApplicationPage} application={app} />
<Can I="create" a="Connection">
<AddAppConnection
onClose={goToApplicationPage}
application={app}
/>
</Can>
}
/>
<Route
path="/connections/:connectionId/reconnect"
element={
<ReconnectConnection
application={app}
onClose={goToApplicationPage}
/>
<Can I="create" a="Connection">
<ReconnectConnection
application={app}
onClose={goToApplicationPage}
/>
</Can>
}
/>
</Routes>

View File

@@ -84,10 +84,14 @@ export default function Applications() {
)}
{!isLoading && !hasApps && (
<NoResultFound
text={formatMessage('apps.noConnections')}
to={URLS.NEW_APP_CONNECTION}
/>
<Can I="create" a="Connection" passThrough>
{(allowed) => (
<NoResultFound
text={formatMessage('apps.noConnections')}
{...(allowed && { to: URLS.NEW_APP_CONNECTION })}
/>
)}
</Can>
)}
{!isLoading &&

View File

@@ -3,7 +3,7 @@ services:
name: automatisch-main
env: docker
dockerfilePath: ./docker/Dockerfile
dockerContext: ./docker
dockerContext: .
repo: https://github.com/automatisch/automatisch
autoDeploy: false
envVars:
@@ -47,7 +47,7 @@ services:
name: automatisch-worker
env: docker
dockerfilePath: ./docker/Dockerfile
dockerContext: ./docker
dockerContext: .
repo: https://github.com/automatisch/automatisch
autoDeploy: false
envVars:

384
yarn.lock
View File

@@ -1455,6 +1455,18 @@
enabled "2.0.x"
kuler "^2.0.0"
"@dagrejs/dagre@^1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@dagrejs/dagre/-/dagre-1.1.2.tgz#5ec339979447091f48d2144deed8c70dfadae374"
integrity sha512-F09dphqvHsbe/6C2t2unbmpr5q41BNPEfJCdn8Z7aEBpVSy/zFQ/b4SWsweQjWNsYMDvE2ffNUN8X0CeFsEGNw==
dependencies:
"@dagrejs/graphlib" "2.2.2"
"@dagrejs/graphlib@2.2.2":
version "2.2.2"
resolved "https://registry.yarnpkg.com/@dagrejs/graphlib/-/graphlib-2.2.2.tgz#74154d5cb880a23b4fae71034a09b4b5aef06feb"
integrity sha512-CbyGpCDKsiTg/wuk79S7Muoj8mghDGAESWGxcSyhHX5jD35vYMBZochYVFzlHxynpE9unpu6O+4ZuhrLxASsOg==
"@docsearch/css@3.2.1", "@docsearch/css@^3.2.1":
version "3.2.1"
resolved "https://registry.npmjs.org/@docsearch/css/-/css-3.2.1.tgz"
@@ -3333,6 +3345,72 @@
resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz"
integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==
"@reactflow/background@11.3.12":
version "11.3.12"
resolved "https://registry.yarnpkg.com/@reactflow/background/-/background-11.3.12.tgz#9c9491cce4659bae13074fcdb48ac25664879d3f"
integrity sha512-jBuWVb43JQy5h4WOS7G0PU8voGTEJNA+qDmx8/jyBtrjbasTesLNfQvboTGjnQYYiJco6mw5vrtQItAJDNoIqw==
dependencies:
"@reactflow/core" "11.11.2"
classcat "^5.0.3"
zustand "^4.4.1"
"@reactflow/controls@11.2.12":
version "11.2.12"
resolved "https://registry.yarnpkg.com/@reactflow/controls/-/controls-11.2.12.tgz#85e2aa5de17e2af28a5ecf6a75bb9c828a20640b"
integrity sha512-L9F3+avFRShoprdT+5oOijm5gVsz2rqWCXBzOAgD923L1XFGIspdiHLLf8IlPGsT+mfl0GxbptZhaEeEzl1e3g==
dependencies:
"@reactflow/core" "11.11.2"
classcat "^5.0.3"
zustand "^4.4.1"
"@reactflow/core@11.11.2":
version "11.11.2"
resolved "https://registry.yarnpkg.com/@reactflow/core/-/core-11.11.2.tgz#c62f78297bda9d2e86a12228617ec3f91fbd4b22"
integrity sha512-+GfgyskweL1PsgRSguUwfrT2eDotlFgaKfDLm7x0brdzzPJY2qbCzVetaxedaiJmIli3817iYbILvE9qLKwbRA==
dependencies:
"@types/d3" "^7.4.0"
"@types/d3-drag" "^3.0.1"
"@types/d3-selection" "^3.0.3"
"@types/d3-zoom" "^3.0.1"
classcat "^5.0.3"
d3-drag "^3.0.0"
d3-selection "^3.0.0"
d3-zoom "^3.0.0"
zustand "^4.4.1"
"@reactflow/minimap@11.7.12":
version "11.7.12"
resolved "https://registry.yarnpkg.com/@reactflow/minimap/-/minimap-11.7.12.tgz#6b2fc671ee17e37ccd3bc038ae8d2121d0ce6291"
integrity sha512-SRDU77c2PCF54PV/MQfkz7VOW46q7V1LZNOQlXAp7dkNyAOI6R+tb9qBUtUJOvILB+TCN6pRfD9fQ+2T99bW3Q==
dependencies:
"@reactflow/core" "11.11.2"
"@types/d3-selection" "^3.0.3"
"@types/d3-zoom" "^3.0.1"
classcat "^5.0.3"
d3-selection "^3.0.0"
d3-zoom "^3.0.0"
zustand "^4.4.1"
"@reactflow/node-resizer@2.2.12":
version "2.2.12"
resolved "https://registry.yarnpkg.com/@reactflow/node-resizer/-/node-resizer-2.2.12.tgz#df82a7dfba883afea6a01a9c8210008a1ddba01f"
integrity sha512-6LHJGuI1zHyRrZHw5gGlVLIWnvVxid9WIqw8FMFSg+oF2DuS3pAPwSoZwypy7W22/gDNl9eD1Dcl/OtFtDFQ+w==
dependencies:
"@reactflow/core" "11.11.2"
classcat "^5.0.4"
d3-drag "^3.0.0"
d3-selection "^3.0.0"
zustand "^4.4.1"
"@reactflow/node-toolbar@1.3.12":
version "1.3.12"
resolved "https://registry.yarnpkg.com/@reactflow/node-toolbar/-/node-toolbar-1.3.12.tgz#89e7aa9d34b6213bb5e64c344d4e2e3cb7af3163"
integrity sha512-4kJRvNna/E3y2MZW9/80wTKwkhw4pLJiz3D5eQrD13XcmojSb1rArO9CiwyrI+rMvs5gn6NlCFB4iN1F+Q+lxQ==
dependencies:
"@reactflow/core" "11.11.2"
classcat "^5.0.3"
zustand "^4.4.1"
"@rollup/plugin-babel@^5.2.0":
version "5.3.0"
resolved "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.0.tgz"
@@ -3823,6 +3901,216 @@
dependencies:
"@types/node" "*"
"@types/d3-array@*":
version "3.2.1"
resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.2.1.tgz#1f6658e3d2006c4fceac53fde464166859f8b8c5"
integrity sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==
"@types/d3-axis@*":
version "3.0.6"
resolved "https://registry.yarnpkg.com/@types/d3-axis/-/d3-axis-3.0.6.tgz#e760e5765b8188b1defa32bc8bb6062f81e4c795"
integrity sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==
dependencies:
"@types/d3-selection" "*"
"@types/d3-brush@*":
version "3.0.6"
resolved "https://registry.yarnpkg.com/@types/d3-brush/-/d3-brush-3.0.6.tgz#c2f4362b045d472e1b186cdbec329ba52bdaee6c"
integrity sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==
dependencies:
"@types/d3-selection" "*"
"@types/d3-chord@*":
version "3.0.6"
resolved "https://registry.yarnpkg.com/@types/d3-chord/-/d3-chord-3.0.6.tgz#1706ca40cf7ea59a0add8f4456efff8f8775793d"
integrity sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==
"@types/d3-color@*":
version "3.1.3"
resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.1.3.tgz#368c961a18de721da8200e80bf3943fb53136af2"
integrity sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==
"@types/d3-contour@*":
version "3.0.6"
resolved "https://registry.yarnpkg.com/@types/d3-contour/-/d3-contour-3.0.6.tgz#9ada3fa9c4d00e3a5093fed0356c7ab929604231"
integrity sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==
dependencies:
"@types/d3-array" "*"
"@types/geojson" "*"
"@types/d3-delaunay@*":
version "6.0.4"
resolved "https://registry.yarnpkg.com/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz#185c1a80cc807fdda2a3fe960f7c11c4a27952e1"
integrity sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==
"@types/d3-dispatch@*":
version "3.0.6"
resolved "https://registry.yarnpkg.com/@types/d3-dispatch/-/d3-dispatch-3.0.6.tgz#096efdf55eb97480e3f5621ff9a8da552f0961e7"
integrity sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==
"@types/d3-drag@*", "@types/d3-drag@^3.0.1":
version "3.0.7"
resolved "https://registry.yarnpkg.com/@types/d3-drag/-/d3-drag-3.0.7.tgz#b13aba8b2442b4068c9a9e6d1d82f8bcea77fc02"
integrity sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==
dependencies:
"@types/d3-selection" "*"
"@types/d3-dsv@*":
version "3.0.7"
resolved "https://registry.yarnpkg.com/@types/d3-dsv/-/d3-dsv-3.0.7.tgz#0a351f996dc99b37f4fa58b492c2d1c04e3dac17"
integrity sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==
"@types/d3-ease@*":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@types/d3-ease/-/d3-ease-3.0.2.tgz#e28db1bfbfa617076f7770dd1d9a48eaa3b6c51b"
integrity sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==
"@types/d3-fetch@*":
version "3.0.7"
resolved "https://registry.yarnpkg.com/@types/d3-fetch/-/d3-fetch-3.0.7.tgz#c04a2b4f23181aa376f30af0283dbc7b3b569980"
integrity sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==
dependencies:
"@types/d3-dsv" "*"
"@types/d3-force@*":
version "3.0.9"
resolved "https://registry.yarnpkg.com/@types/d3-force/-/d3-force-3.0.9.tgz#dd96ccefba4386fe4ff36b8e4ee4e120c21fcf29"
integrity sha512-IKtvyFdb4Q0LWna6ymywQsEYjK/94SGhPrMfEr1TIc5OBeziTi+1jcCvttts8e0UWZIxpasjnQk9MNk/3iS+kA==
"@types/d3-format@*":
version "3.0.4"
resolved "https://registry.yarnpkg.com/@types/d3-format/-/d3-format-3.0.4.tgz#b1e4465644ddb3fdf3a263febb240a6cd616de90"
integrity sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==
"@types/d3-geo@*":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@types/d3-geo/-/d3-geo-3.1.0.tgz#b9e56a079449174f0a2c8684a9a4df3f60522440"
integrity sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==
dependencies:
"@types/geojson" "*"
"@types/d3-hierarchy@*":
version "3.1.7"
resolved "https://registry.yarnpkg.com/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz#6023fb3b2d463229f2d680f9ac4b47466f71f17b"
integrity sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==
"@types/d3-interpolate@*":
version "3.0.4"
resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz#412b90e84870285f2ff8a846c6eb60344f12a41c"
integrity sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==
dependencies:
"@types/d3-color" "*"
"@types/d3-path@*":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-3.1.0.tgz#2b907adce762a78e98828f0b438eaca339ae410a"
integrity sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==
"@types/d3-polygon@*":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@types/d3-polygon/-/d3-polygon-3.0.2.tgz#dfae54a6d35d19e76ac9565bcb32a8e54693189c"
integrity sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==
"@types/d3-quadtree@*":
version "3.0.6"
resolved "https://registry.yarnpkg.com/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz#d4740b0fe35b1c58b66e1488f4e7ed02952f570f"
integrity sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==
"@types/d3-random@*":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/d3-random/-/d3-random-3.0.3.tgz#ed995c71ecb15e0cd31e22d9d5d23942e3300cfb"
integrity sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==
"@types/d3-scale-chromatic@*":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.3.tgz#fc0db9c10e789c351f4c42d96f31f2e4df8f5644"
integrity sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw==
"@types/d3-scale@*":
version "4.0.8"
resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.8.tgz#d409b5f9dcf63074464bf8ddfb8ee5a1f95945bb"
integrity sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==
dependencies:
"@types/d3-time" "*"
"@types/d3-selection@*", "@types/d3-selection@^3.0.3":
version "3.0.10"
resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-3.0.10.tgz#98cdcf986d0986de6912b5892e7c015a95ca27fe"
integrity sha512-cuHoUgS/V3hLdjJOLTT691+G2QoqAjCVLmr4kJXR4ha56w1Zdu8UUQ5TxLRqudgNjwXeQxKMq4j+lyf9sWuslg==
"@types/d3-shape@*":
version "3.1.6"
resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-3.1.6.tgz#65d40d5a548f0a023821773e39012805e6e31a72"
integrity sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==
dependencies:
"@types/d3-path" "*"
"@types/d3-time-format@*":
version "4.0.3"
resolved "https://registry.yarnpkg.com/@types/d3-time-format/-/d3-time-format-4.0.3.tgz#d6bc1e6b6a7db69cccfbbdd4c34b70632d9e9db2"
integrity sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==
"@types/d3-time@*":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.3.tgz#3c186bbd9d12b9d84253b6be6487ca56b54f88be"
integrity sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==
"@types/d3-timer@*":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-3.0.2.tgz#70bbda77dc23aa727413e22e214afa3f0e852f70"
integrity sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==
"@types/d3-transition@*":
version "3.0.8"
resolved "https://registry.yarnpkg.com/@types/d3-transition/-/d3-transition-3.0.8.tgz#677707f5eed5b24c66a1918cde05963021351a8f"
integrity sha512-ew63aJfQ/ms7QQ4X7pk5NxQ9fZH/z+i24ZfJ6tJSfqxJMrYLiK01EAs2/Rtw/JreGUsS3pLPNV644qXFGnoZNQ==
dependencies:
"@types/d3-selection" "*"
"@types/d3-zoom@*", "@types/d3-zoom@^3.0.1":
version "3.0.8"
resolved "https://registry.yarnpkg.com/@types/d3-zoom/-/d3-zoom-3.0.8.tgz#dccb32d1c56b1e1c6e0f1180d994896f038bc40b"
integrity sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==
dependencies:
"@types/d3-interpolate" "*"
"@types/d3-selection" "*"
"@types/d3@^7.4.0":
version "7.4.3"
resolved "https://registry.yarnpkg.com/@types/d3/-/d3-7.4.3.tgz#d4550a85d08f4978faf0a4c36b848c61eaac07e2"
integrity sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==
dependencies:
"@types/d3-array" "*"
"@types/d3-axis" "*"
"@types/d3-brush" "*"
"@types/d3-chord" "*"
"@types/d3-color" "*"
"@types/d3-contour" "*"
"@types/d3-delaunay" "*"
"@types/d3-dispatch" "*"
"@types/d3-drag" "*"
"@types/d3-dsv" "*"
"@types/d3-ease" "*"
"@types/d3-fetch" "*"
"@types/d3-force" "*"
"@types/d3-format" "*"
"@types/d3-geo" "*"
"@types/d3-hierarchy" "*"
"@types/d3-interpolate" "*"
"@types/d3-path" "*"
"@types/d3-polygon" "*"
"@types/d3-quadtree" "*"
"@types/d3-random" "*"
"@types/d3-scale" "*"
"@types/d3-scale-chromatic" "*"
"@types/d3-selection" "*"
"@types/d3-shape" "*"
"@types/d3-time" "*"
"@types/d3-time-format" "*"
"@types/d3-timer" "*"
"@types/d3-transition" "*"
"@types/d3-zoom" "*"
"@types/debug@^4.1.7":
version "4.1.8"
resolved "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz"
@@ -3913,6 +4201,11 @@
"@types/qs" "*"
"@types/serve-static" "*"
"@types/geojson@*":
version "7946.0.14"
resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.14.tgz#319b63ad6df705ee2a65a73ef042c8271e696613"
integrity sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==
"@types/graceful-fs@^4.1.2":
version "4.1.5"
resolved "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz"
@@ -6044,6 +6337,11 @@ cjs-module-lexer@^1.0.0:
resolved "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz"
integrity sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==
classcat@^5.0.3, classcat@^5.0.4:
version "5.0.5"
resolved "https://registry.yarnpkg.com/classcat/-/classcat-5.0.5.tgz#8c209f359a93ac302404a10161b501eba9c09c77"
integrity sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==
clean-css@^5.2.2:
version "5.2.2"
resolved "https://registry.npmjs.org/clean-css/-/clean-css-5.2.2.tgz"
@@ -6829,6 +7127,68 @@ csstype@^3.1.1:
resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz"
integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==
"d3-color@1 - 3":
version "3.1.0"
resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2"
integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==
"d3-dispatch@1 - 3":
version "3.0.1"
resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz#5fc75284e9c2375c36c839411a0cf550cbfc4d5e"
integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==
"d3-drag@2 - 3", d3-drag@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-3.0.0.tgz#994aae9cd23c719f53b5e10e3a0a6108c69607ba"
integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==
dependencies:
d3-dispatch "1 - 3"
d3-selection "3"
"d3-ease@1 - 3":
version "3.0.1"
resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4"
integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==
"d3-interpolate@1 - 3":
version "3.0.1"
resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d"
integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==
dependencies:
d3-color "1 - 3"
"d3-selection@2 - 3", d3-selection@3, d3-selection@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-3.0.0.tgz#c25338207efa72cc5b9bd1458a1a41901f1e1b31"
integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==
"d3-timer@1 - 3":
version "3.0.1"
resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0"
integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==
"d3-transition@2 - 3":
version "3.0.1"
resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-3.0.1.tgz#6869fdde1448868077fdd5989200cb61b2a1645f"
integrity sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==
dependencies:
d3-color "1 - 3"
d3-dispatch "1 - 3"
d3-ease "1 - 3"
d3-interpolate "1 - 3"
d3-timer "1 - 3"
d3-zoom@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-3.0.0.tgz#d13f4165c73217ffeaa54295cd6969b3e7aee8f3"
integrity sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==
dependencies:
d3-dispatch "1 - 3"
d3-drag "2 - 3"
d3-interpolate "1 - 3"
d3-selection "2 - 3"
d3-transition "2 - 3"
damerau-levenshtein@^1.0.7:
version "1.0.8"
resolved "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz"
@@ -13809,6 +14169,18 @@ react@^18.2.0:
dependencies:
loose-envify "^1.1.0"
reactflow@^11.11.2:
version "11.11.2"
resolved "https://registry.yarnpkg.com/reactflow/-/reactflow-11.11.2.tgz#4968866a9372e6004ad1e424a2141996f0ba769a"
integrity sha512-o1fT3stSdhzW+SedCGNSmEvZvULZygZIMLyW67NcWNZrgwx1wuJfzLg5fuQ0Nzf389wItumZX/zP3zdaPX7lEw==
dependencies:
"@reactflow/background" "11.3.12"
"@reactflow/controls" "11.2.12"
"@reactflow/core" "11.11.2"
"@reactflow/minimap" "11.7.12"
"@reactflow/node-resizer" "2.2.12"
"@reactflow/node-toolbar" "1.3.12"
read-cmd-shim@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-2.0.0.tgz"
@@ -15977,6 +16349,11 @@ url-parse-lax@^3.0.0:
dependencies:
prepend-http "^2.0.0"
use-sync-external-store@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"
@@ -16948,3 +17325,10 @@ zen-observable@0.8.15:
version "0.8.15"
resolved "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz"
integrity sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==
zustand@^4.4.1:
version "4.5.2"
resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.5.2.tgz#fddbe7cac1e71d45413b3682cdb47b48034c3848"
integrity sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g==
dependencies:
use-sync-external-store "1.2.0"