Compare commits
40 Commits
AUT-998
...
AUT-157-AU
Author | SHA1 | Date | |
---|---|---|---|
![]() |
8f3ecb6d4d | ||
![]() |
47caa5aa37 | ||
![]() |
725b38c697 | ||
![]() |
402a0fdf3b | ||
![]() |
078364ffa1 | ||
![]() |
f64d5ec4fc | ||
![]() |
12194a50e1 | ||
![]() |
82ee592699 | ||
![]() |
1b4fb2ce6e | ||
![]() |
ebea8d12d1 | ||
![]() |
f842dd77df | ||
![]() |
a6ec7a6c99 | ||
![]() |
369c72282c | ||
![]() |
6f30c1a509 | ||
![]() |
abfd1116c7 | ||
![]() |
017854955d | ||
![]() |
1405cddea1 | ||
![]() |
00dd3164c9 | ||
![]() |
d5cbc0f611 | ||
![]() |
5d2e9ccc67 | ||
![]() |
017a881494 | ||
![]() |
52994970e6 | ||
![]() |
ebae629e5c | ||
![]() |
4d79220b0c | ||
![]() |
96fba7fbb8 | ||
![]() |
e0d610071d | ||
![]() |
ab0966c005 | ||
![]() |
751eb41e72 | ||
![]() |
f08dc25711 | ||
![]() |
737eb31776 | ||
![]() |
d6abf283bc | ||
![]() |
bac4ab5aa4 | ||
![]() |
b5839390fd | ||
![]() |
d19271dae1 | ||
![]() |
ef5a09314e | ||
![]() |
ba52e298eb | ||
![]() |
b3c3998189 | ||
![]() |
782f9b5c04 | ||
![]() |
3079d8c605 | ||
![]() |
c5202d7b3e |
@@ -52,7 +52,7 @@ const appConfig = {
|
|||||||
isDev: appEnv === 'development',
|
isDev: appEnv === 'development',
|
||||||
isTest: appEnv === 'test',
|
isTest: appEnv === 'test',
|
||||||
isProd: appEnv === 'production',
|
isProd: appEnv === 'production',
|
||||||
version: '0.11.0',
|
version: '0.12.0',
|
||||||
postgresDatabase: process.env.POSTGRES_DATABASE || 'automatisch_development',
|
postgresDatabase: process.env.POSTGRES_DATABASE || 'automatisch_development',
|
||||||
postgresSchema: process.env.POSTGRES_SCHEMA || 'public',
|
postgresSchema: process.env.POSTGRES_SCHEMA || 'public',
|
||||||
postgresPort: parseInt(process.env.POSTGRES_PORT || '5432'),
|
postgresPort: parseInt(process.env.POSTGRES_PORT || '5432'),
|
||||||
|
@@ -10,7 +10,7 @@ describe('GET /api/v1/automatisch/version', () => {
|
|||||||
|
|
||||||
const expectedPayload = {
|
const expectedPayload = {
|
||||||
data: {
|
data: {
|
||||||
version: '0.11.0',
|
version: '0.12.0',
|
||||||
},
|
},
|
||||||
meta: {
|
meta: {
|
||||||
count: 1,
|
count: 1,
|
||||||
|
@@ -33,8 +33,8 @@ class User extends Base {
|
|||||||
fullName: { type: 'string', minLength: 1 },
|
fullName: { type: 'string', minLength: 1 },
|
||||||
email: { type: 'string', format: 'email', minLength: 1, maxLength: 255 },
|
email: { type: 'string', format: 'email', minLength: 1, maxLength: 255 },
|
||||||
password: { type: 'string' },
|
password: { type: 'string' },
|
||||||
resetPasswordToken: { type: 'string' },
|
resetPasswordToken: { type: ['string', 'null'] },
|
||||||
resetPasswordTokenSentAt: { type: 'string' },
|
resetPasswordTokenSentAt: { type: ['string', 'null'], format: 'date-time' },
|
||||||
trialExpiryDate: { type: 'string' },
|
trialExpiryDate: { type: 'string' },
|
||||||
roleId: { type: 'string', format: 'uuid' },
|
roleId: { type: 'string', format: 'uuid' },
|
||||||
deletedAt: { type: 'string' },
|
deletedAt: { type: 'string' },
|
||||||
|
@@ -40,6 +40,7 @@ export const worker = new Worker(
|
|||||||
await user.$relatedQuery('usageData').withSoftDeleted().hardDelete();
|
await user.$relatedQuery('usageData').withSoftDeleted().hardDelete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await user.$relatedQuery('accessTokens').withSoftDeleted().hardDelete();
|
||||||
await user.$query().withSoftDeleted().hardDelete();
|
await user.$query().withSoftDeleted().hardDelete();
|
||||||
},
|
},
|
||||||
{ connection: redisConfig }
|
{ connection: redisConfig }
|
||||||
|
@@ -6,16 +6,12 @@ We use `lerna` with `yarn workspaces` to manage the mono repository. We have the
|
|||||||
.
|
.
|
||||||
├── packages
|
├── packages
|
||||||
│ ├── backend
|
│ ├── backend
|
||||||
│ ├── cli
|
|
||||||
│ ├── docs
|
│ ├── docs
|
||||||
│ ├── e2e-tests
|
│ ├── e2e-tests
|
||||||
│ ├── types
|
|
||||||
│ └── web
|
│ └── web
|
||||||
```
|
```
|
||||||
|
|
||||||
- `backend` - The backend package contains the backend application and all integrations.
|
- `backend` - The backend package contains the backend application and all integrations.
|
||||||
- `cli` - The cli package contains the CLI application of Automatisch.
|
|
||||||
- `docs` - The docs package contains the documentation website.
|
- `docs` - The docs package contains the documentation website.
|
||||||
- `e2e-tests` - The e2e-tests package contains the end-to-end tests for the internal usage.
|
- `e2e-tests` - The e2e-tests package contains the end-to-end tests for the internal usage.
|
||||||
- `types` - The types package contains the shared types for both the backend and web packages.
|
|
||||||
- `web` - The web package contains the frontend application of Automatisch.
|
- `web` - The web package contains the frontend application of Automatisch.
|
||||||
|
@@ -7,6 +7,7 @@
|
|||||||
"@apollo/client": "^3.6.9",
|
"@apollo/client": "^3.6.9",
|
||||||
"@casl/ability": "^6.5.0",
|
"@casl/ability": "^6.5.0",
|
||||||
"@casl/react": "^3.1.0",
|
"@casl/react": "^3.1.0",
|
||||||
|
"@dagrejs/dagre": "^1.1.2",
|
||||||
"@emotion/react": "^11.4.1",
|
"@emotion/react": "^11.4.1",
|
||||||
"@emotion/styled": "^11.3.0",
|
"@emotion/styled": "^11.3.0",
|
||||||
"@hookform/resolvers": "^2.8.8",
|
"@hookform/resolvers": "^2.8.8",
|
||||||
@@ -32,6 +33,7 @@
|
|||||||
"react-router-dom": "^6.0.2",
|
"react-router-dom": "^6.0.2",
|
||||||
"react-scripts": "5.0.0",
|
"react-scripts": "5.0.0",
|
||||||
"react-window": "^1.8.9",
|
"react-window": "^1.8.9",
|
||||||
|
"reactflow": "^11.11.2",
|
||||||
"slate": "^0.94.1",
|
"slate": "^0.94.1",
|
||||||
"slate-history": "^0.93.0",
|
"slate-history": "^0.93.0",
|
||||||
"slate-react": "^0.94.2",
|
"slate-react": "^0.94.2",
|
||||||
|
@@ -68,7 +68,10 @@ function AccountDropdownMenu(props) {
|
|||||||
AccountDropdownMenu.propTypes = {
|
AccountDropdownMenu.propTypes = {
|
||||||
open: PropTypes.bool.isRequired,
|
open: PropTypes.bool.isRequired,
|
||||||
onClose: PropTypes.func.isRequired,
|
onClose: PropTypes.func.isRequired,
|
||||||
anchorEl: PropTypes.oneOfType([PropTypes.element, PropTypes.func]),
|
anchorEl: PropTypes.oneOfType([
|
||||||
|
PropTypes.func,
|
||||||
|
PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
|
||||||
|
]),
|
||||||
id: PropTypes.string.isRequired,
|
id: PropTypes.string.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -36,7 +36,7 @@ function AdminApplicationSettings(props) {
|
|||||||
|
|
||||||
const handleSubmit = async (values) => {
|
const handleSubmit = async (values) => {
|
||||||
try {
|
try {
|
||||||
if (!appConfig.data) {
|
if (!appConfig?.data) {
|
||||||
await createAppConfig({
|
await createAppConfig({
|
||||||
variables: {
|
variables: {
|
||||||
input: { key: props.appKey, ...values },
|
input: { key: props.appKey, ...values },
|
||||||
@@ -69,6 +69,7 @@ function AdminApplicationSettings(props) {
|
|||||||
}),
|
}),
|
||||||
[appConfig?.data],
|
[appConfig?.data],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form
|
<Form
|
||||||
defaultValues={defaultValues}
|
defaultValues={defaultValues}
|
||||||
|
@@ -8,6 +8,7 @@ import * as URLS from 'config/urls';
|
|||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
import { ConnectionPropType } from 'propTypes/propTypes';
|
import { ConnectionPropType } from 'propTypes/propTypes';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import Can from 'components/Can';
|
||||||
|
|
||||||
function ContextMenu(props) {
|
function ContextMenu(props) {
|
||||||
const {
|
const {
|
||||||
@@ -44,34 +45,57 @@ function ContextMenu(props) {
|
|||||||
hideBackdrop={false}
|
hideBackdrop={false}
|
||||||
anchorEl={anchorEl}
|
anchorEl={anchorEl}
|
||||||
>
|
>
|
||||||
<MenuItem
|
<Can I="read" a="Flow" passThrough>
|
||||||
component={Link}
|
{(allowed) => (
|
||||||
to={URLS.APP_FLOWS_FOR_CONNECTION(appKey, connection.id)}
|
<MenuItem
|
||||||
onClick={createActionHandler({ type: 'viewFlows' })}
|
component={Link}
|
||||||
>
|
to={URLS.APP_FLOWS_FOR_CONNECTION(appKey, connection.id)}
|
||||||
{formatMessage('connection.viewFlows')}
|
onClick={createActionHandler({ type: 'viewFlows' })}
|
||||||
</MenuItem>
|
disabled={!allowed}
|
||||||
|
>
|
||||||
<MenuItem onClick={createActionHandler({ type: 'test' })}>
|
{formatMessage('connection.viewFlows')}
|
||||||
{formatMessage('connection.testConnection')}
|
</MenuItem>
|
||||||
</MenuItem>
|
|
||||||
|
|
||||||
<MenuItem
|
|
||||||
component={Link}
|
|
||||||
disabled={disableReconnection}
|
|
||||||
to={URLS.APP_RECONNECT_CONNECTION(
|
|
||||||
appKey,
|
|
||||||
connection.id,
|
|
||||||
connection.appAuthClientId,
|
|
||||||
)}
|
)}
|
||||||
onClick={createActionHandler({ type: 'reconnect' })}
|
</Can>
|
||||||
>
|
|
||||||
{formatMessage('connection.reconnect')}
|
|
||||||
</MenuItem>
|
|
||||||
|
|
||||||
<MenuItem onClick={createActionHandler({ type: 'delete' })}>
|
<Can I="update" a="Connection" passThrough>
|
||||||
{formatMessage('connection.delete')}
|
{(allowed) => (
|
||||||
</MenuItem>
|
<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>
|
</Menu>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
|||||||
|
|
||||||
import AppConnectionRow from 'components/AppConnectionRow';
|
import AppConnectionRow from 'components/AppConnectionRow';
|
||||||
import NoResultFound from 'components/NoResultFound';
|
import NoResultFound from 'components/NoResultFound';
|
||||||
|
import Can from 'components/Can';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
import * as URLS from 'config/urls';
|
import * as URLS from 'config/urls';
|
||||||
import useAppConnections from 'hooks/useAppConnections';
|
import useAppConnections from 'hooks/useAppConnections';
|
||||||
@@ -16,11 +17,15 @@ function AppConnections(props) {
|
|||||||
|
|
||||||
if (!hasConnections) {
|
if (!hasConnections) {
|
||||||
return (
|
return (
|
||||||
<NoResultFound
|
<Can I="create" a="Connection" passThrough>
|
||||||
to={URLS.APP_ADD_CONNECTION(appKey)}
|
{(allowed) => (
|
||||||
text={formatMessage('app.noConnections')}
|
<NoResultFound
|
||||||
data-test="connections-no-results"
|
text={formatMessage('app.noConnections')}
|
||||||
/>
|
data-test="connections-no-results"
|
||||||
|
{...(allowed && { to: URLS.APP_ADD_CONNECTION(appKey) })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Can>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -5,6 +5,7 @@ import PaginationItem from '@mui/material/PaginationItem';
|
|||||||
|
|
||||||
import * as URLS from 'config/urls';
|
import * as URLS from 'config/urls';
|
||||||
import AppFlowRow from 'components/FlowRow';
|
import AppFlowRow from 'components/FlowRow';
|
||||||
|
import Can from 'components/Can';
|
||||||
import NoResultFound from 'components/NoResultFound';
|
import NoResultFound from 'components/NoResultFound';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
import useConnectionFlows from 'hooks/useConnectionFlows';
|
import useConnectionFlows from 'hooks/useConnectionFlows';
|
||||||
@@ -36,11 +37,20 @@ function AppFlows(props) {
|
|||||||
|
|
||||||
if (!hasFlows) {
|
if (!hasFlows) {
|
||||||
return (
|
return (
|
||||||
<NoResultFound
|
<Can I="create" a="Flow" passThrough>
|
||||||
to={URLS.CREATE_FLOW_WITH_APP_AND_CONNECTION(appKey, connectionId)}
|
{(allowed) => (
|
||||||
text={formatMessage('app.noFlows')}
|
<NoResultFound
|
||||||
data-test="flows-no-results"
|
text={formatMessage('app.noFlows')}
|
||||||
/>
|
data-test="flows-no-results"
|
||||||
|
{...(allowed && {
|
||||||
|
to: URLS.CREATE_FLOW_WITH_APP_AND_CONNECTION(
|
||||||
|
appKey,
|
||||||
|
connectionId
|
||||||
|
),
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Can>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -165,6 +165,7 @@ function ChooseAppAndEventSubstep(props) {
|
|||||||
value={getOption(appOptions, step.appKey) || null}
|
value={getOption(appOptions, step.appKey) || null}
|
||||||
onChange={onAppChange}
|
onChange={onAppChange}
|
||||||
data-test="choose-app-autocomplete"
|
data-test="choose-app-autocomplete"
|
||||||
|
componentsProps={{ popper: { className: 'nowheel' } }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{step.appKey && (
|
{step.appKey && (
|
||||||
@@ -227,6 +228,7 @@ function ChooseAppAndEventSubstep(props) {
|
|||||||
value={getOption(actionOrTriggerOptions, step.key) || null}
|
value={getOption(actionOrTriggerOptions, step.key) || null}
|
||||||
onChange={onEventChange}
|
onChange={onEventChange}
|
||||||
data-test="choose-event-autocomplete"
|
data-test="choose-event-autocomplete"
|
||||||
|
componentsProps={{ popper: { className: 'nowheel' } }}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
@@ -240,6 +240,7 @@ function ChooseConnectionSubstep(props) {
|
|||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
loading={isAppConnectionsLoading}
|
loading={isAppConnectionsLoading}
|
||||||
data-test="choose-connection-autocomplete"
|
data-test="choose-connection-autocomplete"
|
||||||
|
componentsProps={{ popper: { className: 'nowheel' } }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
@@ -32,9 +32,11 @@ function ControlledAutocomplete(props) {
|
|||||||
...autocompleteProps
|
...autocompleteProps
|
||||||
} = props;
|
} = props;
|
||||||
let dependsOnValues = [];
|
let dependsOnValues = [];
|
||||||
|
|
||||||
if (dependsOn?.length) {
|
if (dependsOn?.length) {
|
||||||
dependsOnValues = watch(dependsOn);
|
dependsOnValues = watch(dependsOn);
|
||||||
}
|
}
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const hasDependencies = dependsOnValues.length;
|
const hasDependencies = dependsOnValues.length;
|
||||||
const allDepsSatisfied = dependsOnValues.every(Boolean);
|
const allDepsSatisfied = dependsOnValues.every(Boolean);
|
||||||
@@ -44,6 +46,7 @@ function ControlledAutocomplete(props) {
|
|||||||
resetField(name);
|
resetField(name);
|
||||||
}
|
}
|
||||||
}, dependsOnValues);
|
}, dependsOnValues);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Controller
|
<Controller
|
||||||
rules={{ required }}
|
rules={{ required }}
|
||||||
|
@@ -21,7 +21,9 @@ const CustomOptions = (props) => {
|
|||||||
label,
|
label,
|
||||||
initialTabIndex,
|
initialTabIndex,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [activeTabIndex, setActiveTabIndex] = React.useState(undefined);
|
const [activeTabIndex, setActiveTabIndex] = React.useState(undefined);
|
||||||
|
|
||||||
React.useEffect(
|
React.useEffect(
|
||||||
function applyInitialActiveTabIndex() {
|
function applyInitialActiveTabIndex() {
|
||||||
setActiveTabIndex((currentActiveTabIndex) => {
|
setActiveTabIndex((currentActiveTabIndex) => {
|
||||||
@@ -33,6 +35,7 @@ const CustomOptions = (props) => {
|
|||||||
},
|
},
|
||||||
[initialTabIndex],
|
[initialTabIndex],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popper
|
<Popper
|
||||||
open={open}
|
open={open}
|
||||||
@@ -47,6 +50,7 @@ const CustomOptions = (props) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
className="nowheel"
|
||||||
>
|
>
|
||||||
<Paper elevation={5} sx={{ width: '100%' }}>
|
<Paper elevation={5} sx={{ width: '100%' }}>
|
||||||
<Tabs
|
<Tabs
|
||||||
@@ -75,7 +79,10 @@ const CustomOptions = (props) => {
|
|||||||
|
|
||||||
CustomOptions.propTypes = {
|
CustomOptions.propTypes = {
|
||||||
open: PropTypes.bool.isRequired,
|
open: PropTypes.bool.isRequired,
|
||||||
anchorEl: PropTypes.oneOfType([PropTypes.element, PropTypes.func]).isRequired,
|
anchorEl: PropTypes.oneOfType([
|
||||||
|
PropTypes.func,
|
||||||
|
PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
|
||||||
|
]),
|
||||||
data: PropTypes.arrayOf(
|
data: PropTypes.arrayOf(
|
||||||
PropTypes.shape({
|
PropTypes.shape({
|
||||||
id: PropTypes.string.isRequired,
|
id: PropTypes.string.isRequired,
|
||||||
|
@@ -61,6 +61,7 @@ function ControlledCustomAutocomplete(props) {
|
|||||||
const [isSingleChoice, setSingleChoice] = React.useState(undefined);
|
const [isSingleChoice, setSingleChoice] = React.useState(undefined);
|
||||||
const priorStepsWithExecutions = React.useContext(StepExecutionsContext);
|
const priorStepsWithExecutions = React.useContext(StepExecutionsContext);
|
||||||
const editorRef = React.useRef(null);
|
const editorRef = React.useRef(null);
|
||||||
|
const mountedRef = React.useRef(false);
|
||||||
|
|
||||||
const renderElement = React.useCallback(
|
const renderElement = React.useCallback(
|
||||||
(props) => <Element {...props} disabled={disabled} />,
|
(props) => <Element {...props} disabled={disabled} />,
|
||||||
@@ -94,10 +95,14 @@ function ControlledCustomAutocomplete(props) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const hasDependencies = dependsOnValues.length;
|
if (mountedRef.current) {
|
||||||
if (hasDependencies) {
|
const hasDependencies = dependsOnValues.length;
|
||||||
// Reset the field when a dependent has been updated
|
if (hasDependencies) {
|
||||||
resetEditor(editor);
|
// Reset the field when a dependent has been updated
|
||||||
|
resetEditor(editor);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mountedRef.current = true;
|
||||||
}
|
}
|
||||||
}, dependsOnValues);
|
}, dependsOnValues);
|
||||||
|
|
||||||
|
@@ -64,11 +64,19 @@ function DynamicField(props) {
|
|||||||
<Stack
|
<Stack
|
||||||
direction={{ xs: 'column', sm: 'row' }}
|
direction={{ xs: 'column', sm: 'row' }}
|
||||||
spacing={{ xs: 2 }}
|
spacing={{ xs: 2 }}
|
||||||
sx={{ display: 'flex', flex: 1 }}
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flex: 1,
|
||||||
|
minWidth: 0,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{fields.map((fieldSchema, fieldSchemaIndex) => (
|
{fields.map((fieldSchema, fieldSchemaIndex) => (
|
||||||
<Box
|
<Box
|
||||||
sx={{ display: 'flex', flex: '1 0 0px' }}
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flex: '1 0 0px',
|
||||||
|
minWidth: 0,
|
||||||
|
}}
|
||||||
key={`field-${field.__id}-${fieldSchemaIndex}`}
|
key={`field-${field.__id}-${fieldSchemaIndex}`}
|
||||||
>
|
>
|
||||||
<InputCreator
|
<InputCreator
|
||||||
|
@@ -8,6 +8,7 @@ import Tooltip from '@mui/material/Tooltip';
|
|||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew';
|
import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew';
|
||||||
import Snackbar from '@mui/material/Snackbar';
|
import Snackbar from '@mui/material/Snackbar';
|
||||||
|
import { ReactFlowProvider } from 'reactflow';
|
||||||
|
|
||||||
import { EditorProvider } from 'contexts/Editor';
|
import { EditorProvider } from 'contexts/Editor';
|
||||||
import EditableTypography from 'components/EditableTypography';
|
import EditableTypography from 'components/EditableTypography';
|
||||||
@@ -20,6 +21,9 @@ import * as URLS from 'config/urls';
|
|||||||
import { TopBar } from './style';
|
import { TopBar } from './style';
|
||||||
import useFlow from 'hooks/useFlow';
|
import useFlow from 'hooks/useFlow';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
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() {
|
export default function EditorLayout() {
|
||||||
const { flowId } = useParams();
|
const { flowId } = useParams();
|
||||||
@@ -55,23 +59,25 @@ export default function EditorLayout() {
|
|||||||
|
|
||||||
const onFlowStatusUpdate = React.useCallback(
|
const onFlowStatusUpdate = React.useCallback(
|
||||||
async (active) => {
|
async (active) => {
|
||||||
await updateFlowStatus({
|
try {
|
||||||
variables: {
|
await updateFlowStatus({
|
||||||
input: {
|
variables: {
|
||||||
id: flowId,
|
input: {
|
||||||
active,
|
id: flowId,
|
||||||
|
active,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
optimisticResponse: {
|
||||||
optimisticResponse: {
|
updateFlowStatus: {
|
||||||
updateFlowStatus: {
|
__typename: 'Flow',
|
||||||
__typename: 'Flow',
|
id: flowId,
|
||||||
id: flowId,
|
active,
|
||||||
active,
|
},
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
});
|
|
||||||
|
|
||||||
await queryClient.invalidateQueries({ queryKey: ['flows', flowId] });
|
await queryClient.invalidateQueries({ queryKey: ['flows', flowId] });
|
||||||
|
} catch (err) {}
|
||||||
},
|
},
|
||||||
[flowId, queryClient],
|
[flowId, queryClient],
|
||||||
);
|
);
|
||||||
@@ -131,15 +137,28 @@ export default function EditorLayout() {
|
|||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</TopBar>
|
</TopBar>
|
||||||
<Stack direction="column" height="100%">
|
|
||||||
<Container maxWidth="md">
|
|
||||||
<EditorProvider value={{ readOnly: !!flow?.active }}>
|
|
||||||
{!flow && !isFlowLoading && 'not found'}
|
|
||||||
|
|
||||||
{flow && <Editor flow={flow} />}
|
{useNewFlowEditor ? (
|
||||||
</EditorProvider>
|
<Stack direction="column" height="100%" flexGrow={1}>
|
||||||
</Container>
|
<Stack direction="column" flexGrow={1}>
|
||||||
</Stack>
|
<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
|
<Snackbar
|
||||||
data-test="flow-cannot-edit-info-snackbar"
|
data-test="flow-cannot-edit-info-snackbar"
|
||||||
|
57
packages/web/src/components/EditorNew/Edge/Edge.jsx
Normal file
57
packages/web/src/components/EditorNew/Edge/Edge.jsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { EdgeLabelRenderer, getStraightPath } 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';
|
||||||
|
|
||||||
|
export default function Edge({
|
||||||
|
sourceX,
|
||||||
|
sourceY,
|
||||||
|
targetX,
|
||||||
|
targetY,
|
||||||
|
source,
|
||||||
|
data: { laidOut },
|
||||||
|
}) {
|
||||||
|
const { stepCreationInProgress, flowActive, onAddStep } =
|
||||||
|
useContext(EdgesContext);
|
||||||
|
|
||||||
|
const [edgePath, labelX, labelY] = getStraightPath({
|
||||||
|
sourceX,
|
||||||
|
sourceY,
|
||||||
|
targetX,
|
||||||
|
targetY,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<EdgeLabelRenderer>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => onAddStep(source)}
|
||||||
|
color="primary"
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
|
||||||
|
pointerEvents: 'all',
|
||||||
|
visibility: laidOut ? 'visible' : 'hidden',
|
||||||
|
}}
|
||||||
|
disabled={stepCreationInProgress || flowActive}
|
||||||
|
>
|
||||||
|
<AddIcon />
|
||||||
|
</IconButton>
|
||||||
|
</EdgeLabelRenderer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Edge.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,
|
||||||
|
};
|
277
packages/web/src/components/EditorNew/EditorNew.jsx
Normal file
277
packages/web/src/components/EditorNew/EditorNew.jsx
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
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 { useScrollBoundaries } from './useScrollBoundaries';
|
||||||
|
import FlowStepNode from './FlowStepNode/FlowStepNode';
|
||||||
|
import Edge from './Edge/Edge';
|
||||||
|
import InvisibleNode from './InvisibleNode/InvisibleNode';
|
||||||
|
import { EditorWrapper } from './style';
|
||||||
|
import {
|
||||||
|
generateEdgeId,
|
||||||
|
generateInitialEdges,
|
||||||
|
generateInitialNodes,
|
||||||
|
updatedCollapsedNodes,
|
||||||
|
} from './utils';
|
||||||
|
import { EDGE_TYPES, INVISIBLE_NODE_ID, NODE_TYPES } from './constants';
|
||||||
|
|
||||||
|
export const EdgesContext = createContext();
|
||||||
|
export const NodesContext = createContext();
|
||||||
|
|
||||||
|
const nodeTypes = {
|
||||||
|
[NODE_TYPES.FLOW_STEP]: FlowStepNode,
|
||||||
|
[NODE_TYPES.INVISIBLE]: InvisibleNode,
|
||||||
|
};
|
||||||
|
|
||||||
|
const edgeTypes = {
|
||||||
|
[EDGE_TYPES.ADD_NODE_EDGE]: Edge,
|
||||||
|
};
|
||||||
|
|
||||||
|
const EditorNew = ({ flow }) => {
|
||||||
|
const [updateStep] = useMutation(UPDATE_STEP);
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const [createStep, { loading: stepCreationInProgress }] =
|
||||||
|
useMutation(CREATE_STEP);
|
||||||
|
|
||||||
|
const [nodes, setNodes, onNodesChange] = useNodesState(
|
||||||
|
generateInitialNodes(flow),
|
||||||
|
);
|
||||||
|
const [edges, setEdges, onEdgesChange] = useEdgesState(
|
||||||
|
generateInitialEdges(flow),
|
||||||
|
);
|
||||||
|
|
||||||
|
useAutoLayout();
|
||||||
|
useScrollBoundaries();
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = useCallback(
|
||||||
|
async (previousStepId) => {
|
||||||
|
const mutationInput = {
|
||||||
|
previousStep: {
|
||||||
|
id: previousStepId,
|
||||||
|
},
|
||||||
|
flow: {
|
||||||
|
id: flow.id,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: { createStep: createdStep },
|
||||||
|
} = await createStep({
|
||||||
|
variables: { input: mutationInput },
|
||||||
|
});
|
||||||
|
|
||||||
|
const createdStepId = createdStep.id;
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ['flows', flow.id] });
|
||||||
|
createdStepIdRef.current = createdStepId;
|
||||||
|
},
|
||||||
|
[flow.id, createStep, queryClient],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (flow.steps.length + 1 !== nodes.length) {
|
||||||
|
setNodes((nodes) => {
|
||||||
|
const newNodes = flow.steps.map((step) => {
|
||||||
|
const createdStepId = createdStepIdRef.current;
|
||||||
|
const prevNode = nodes.find(({ id }) => id === step.id);
|
||||||
|
if (prevNode) {
|
||||||
|
return {
|
||||||
|
...prevNode,
|
||||||
|
zIndex: createdStepId ? 0 : prevNode.zIndex,
|
||||||
|
data: {
|
||||||
|
...prevNode.data,
|
||||||
|
collapsed: createdStepId ? true : prevNode.data.collapsed,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
id: step.id,
|
||||||
|
type: NODE_TYPES.FLOW_STEP,
|
||||||
|
position: {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
zIndex: 1,
|
||||||
|
data: {
|
||||||
|
collapsed: false,
|
||||||
|
laidOut: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const prevInvisible = nodes.find(({ id }) => id === INVISIBLE_NODE_ID);
|
||||||
|
return [
|
||||||
|
...newNodes,
|
||||||
|
{
|
||||||
|
id: INVISIBLE_NODE_ID,
|
||||||
|
type: NODE_TYPES.INVISIBLE,
|
||||||
|
position: {
|
||||||
|
x: prevInvisible?.position.x || 0,
|
||||||
|
y: prevInvisible?.position.y || 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
setEdges((edges) => {
|
||||||
|
const newEdges = flow.steps
|
||||||
|
.map((step, i) => {
|
||||||
|
const sourceId = step.id;
|
||||||
|
const targetId = flow.steps[i + 1]?.id;
|
||||||
|
const edge = edges?.find(
|
||||||
|
(edge) => edge.id === generateEdgeId(sourceId, targetId),
|
||||||
|
);
|
||||||
|
if (targetId) {
|
||||||
|
return {
|
||||||
|
id: generateEdgeId(sourceId, targetId),
|
||||||
|
source: sourceId,
|
||||||
|
target: targetId,
|
||||||
|
type: 'addNodeEdge',
|
||||||
|
data: {
|
||||||
|
laidOut: edge ? edge?.data.laidOut : false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
.filter((edge) => !!edge);
|
||||||
|
|
||||||
|
const lastStep = flow.steps[flow.steps.length - 1];
|
||||||
|
const lastEdge = edges[edges.length - 1];
|
||||||
|
|
||||||
|
return lastStep
|
||||||
|
? [
|
||||||
|
...newEdges,
|
||||||
|
{
|
||||||
|
id: generateEdgeId(lastStep.id, INVISIBLE_NODE_ID),
|
||||||
|
source: lastStep.id,
|
||||||
|
target: INVISIBLE_NODE_ID,
|
||||||
|
type: 'addNodeEdge',
|
||||||
|
data: {
|
||||||
|
laidOut:
|
||||||
|
lastEdge?.id ===
|
||||||
|
generateEdgeId(lastStep.id, INVISIBLE_NODE_ID)
|
||||||
|
? lastEdge?.data.laidOut
|
||||||
|
: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: newEdges;
|
||||||
|
});
|
||||||
|
|
||||||
|
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,
|
||||||
|
flowActive: flow.active,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<EditorWrapper direction="column">
|
||||||
|
<ReactFlow
|
||||||
|
nodes={nodes}
|
||||||
|
edges={edges}
|
||||||
|
onNodesChange={onNodesChange}
|
||||||
|
onEdgesChange={onEdgesChange}
|
||||||
|
nodeTypes={nodeTypes}
|
||||||
|
edgeTypes={edgeTypes}
|
||||||
|
panOnScroll
|
||||||
|
panOnScrollMode="vertical"
|
||||||
|
panOnDrag={false}
|
||||||
|
zoomOnScroll={false}
|
||||||
|
zoomOnPinch={false}
|
||||||
|
zoomOnDoubleClick={false}
|
||||||
|
panActivationKeyCode={null}
|
||||||
|
proOptions={{ hideAttribution: true }}
|
||||||
|
/>
|
||||||
|
</EditorWrapper>
|
||||||
|
</EdgesContext.Provider>
|
||||||
|
</NodesContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
EditorNew.propTypes = {
|
||||||
|
flow: FlowPropType.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditorNew;
|
@@ -0,0 +1,60 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
function FlowStepNode({ data: { collapsed, laidOut }, id }) {
|
||||||
|
const { openNextStep, onStepOpen, onStepClose, onStepChange, flowId, steps } =
|
||||||
|
useContext(NodesContext);
|
||||||
|
|
||||||
|
const step = steps.find(({ id: stepId }) => stepId === id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NodeWrapper
|
||||||
|
className="nodrag"
|
||||||
|
sx={{
|
||||||
|
visibility: laidOut ? 'visible' : 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<NodeInnerWrapper>
|
||||||
|
<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)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<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;
|
14
packages/web/src/components/EditorNew/FlowStepNode/style.js
Normal file
14
packages/web/src/components/EditorNew/FlowStepNode/style.js
Normal 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 }) => ({
|
||||||
|
maxWidth: 900,
|
||||||
|
flex: 1,
|
||||||
|
}));
|
@@ -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;
|
10
packages/web/src/components/EditorNew/constants.js
Normal file
10
packages/web/src/components/EditorNew/constants.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export const INVISIBLE_NODE_ID = 'invisible-node';
|
||||||
|
|
||||||
|
export const NODE_TYPES = {
|
||||||
|
FLOW_STEP: 'flowStep',
|
||||||
|
INVISIBLE: 'invisible',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EDGE_TYPES = {
|
||||||
|
ADD_NODE_EDGE: 'addNodeEdge',
|
||||||
|
};
|
13
packages/web/src/components/EditorNew/style.js
Normal file
13
packages/web/src/components/EditorNew/style.js
Normal 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',
|
||||||
|
},
|
||||||
|
}));
|
69
packages/web/src/components/EditorNew/useAutoLayout.js
Normal file
69
packages/web/src/components/EditorNew/useAutoLayout.js
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
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 } = 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 })),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (shouldAutoLayout) {
|
||||||
|
onLayout(nodes, getEdges());
|
||||||
|
}
|
||||||
|
}, [nodes]);
|
||||||
|
};
|
13
packages/web/src/components/EditorNew/useScrollBoundaries.js
Normal file
13
packages/web/src/components/EditorNew/useScrollBoundaries.js
Normal 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]);
|
||||||
|
};
|
88
packages/web/src/components/EditorNew/utils.js
Normal file
88
packages/web/src/components/EditorNew/utils.js
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import { 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 generateInitialNodes = (flow) => {
|
||||||
|
const newNodes = flow.steps.map((step, index) => {
|
||||||
|
const collapsed = index !== 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: step.id,
|
||||||
|
type: NODE_TYPES.FLOW_STEP,
|
||||||
|
position: {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
zIndex: collapsed ? 0 : 1,
|
||||||
|
data: {
|
||||||
|
collapsed,
|
||||||
|
laidOut: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return [
|
||||||
|
...newNodes,
|
||||||
|
{
|
||||||
|
id: INVISIBLE_NODE_ID,
|
||||||
|
type: NODE_TYPES.INVISIBLE,
|
||||||
|
position: {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateInitialEdges = (flow) => {
|
||||||
|
const newEdges = flow.steps
|
||||||
|
.map((step, i) => {
|
||||||
|
const sourceId = step.id;
|
||||||
|
const targetId = flow.steps[i + 1]?.id;
|
||||||
|
if (targetId) {
|
||||||
|
return {
|
||||||
|
id: generateEdgeId(sourceId, targetId),
|
||||||
|
source: sourceId,
|
||||||
|
target: targetId,
|
||||||
|
type: 'addNodeEdge',
|
||||||
|
data: {
|
||||||
|
laidOut: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
.filter((edge) => !!edge);
|
||||||
|
|
||||||
|
const lastStep = flow.steps[flow.steps.length - 1];
|
||||||
|
|
||||||
|
return lastStep
|
||||||
|
? [
|
||||||
|
...newEdges,
|
||||||
|
{
|
||||||
|
id: generateEdgeId(lastStep.id, INVISIBLE_NODE_ID),
|
||||||
|
source: lastStep.id,
|
||||||
|
target: INVISIBLE_NODE_ID,
|
||||||
|
type: 'addNodeEdge',
|
||||||
|
data: {
|
||||||
|
laidOut: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: newEdges;
|
||||||
|
};
|
@@ -28,9 +28,12 @@ function ContextMenu(props) {
|
|||||||
variables: { input: { id: flowId } },
|
variables: { input: { id: flowId } },
|
||||||
});
|
});
|
||||||
|
|
||||||
await queryClient.invalidateQueries({
|
if (appKey) {
|
||||||
queryKey: ['apps', appKey, 'flows'],
|
await queryClient.invalidateQueries({
|
||||||
});
|
queryKey: ['apps', appKey, 'flows'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
enqueueSnackbar(formatMessage('flow.successfullyDuplicated'), {
|
enqueueSnackbar(formatMessage('flow.successfullyDuplicated'), {
|
||||||
variant: 'success',
|
variant: 'success',
|
||||||
SnackbarProps: {
|
SnackbarProps: {
|
||||||
@@ -56,9 +59,12 @@ function ContextMenu(props) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await queryClient.invalidateQueries({
|
if (appKey) {
|
||||||
queryKey: ['apps', appKey, 'flows'],
|
await queryClient.invalidateQueries({
|
||||||
});
|
queryKey: ['apps', appKey, 'flows'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
enqueueSnackbar(formatMessage('flow.successfullyDeleted'), {
|
enqueueSnackbar(formatMessage('flow.successfullyDeleted'), {
|
||||||
variant: 'success',
|
variant: 'success',
|
||||||
});
|
});
|
||||||
@@ -110,7 +116,7 @@ ContextMenu.propTypes = {
|
|||||||
]).isRequired,
|
]).isRequired,
|
||||||
onDeleteFlow: PropTypes.func,
|
onDeleteFlow: PropTypes.func,
|
||||||
onDuplicateFlow: PropTypes.func,
|
onDuplicateFlow: PropTypes.func,
|
||||||
appKey: PropTypes.string.isRequired,
|
appKey: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ContextMenu;
|
export default ContextMenu;
|
||||||
|
@@ -38,20 +38,24 @@ function FlowRow(props) {
|
|||||||
const contextButtonRef = React.useRef(null);
|
const contextButtonRef = React.useRef(null);
|
||||||
const [anchorEl, setAnchorEl] = React.useState(null);
|
const [anchorEl, setAnchorEl] = React.useState(null);
|
||||||
const { flow, onDuplicateFlow, onDeleteFlow, appKey } = props;
|
const { flow, onDuplicateFlow, onDeleteFlow, appKey } = props;
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setAnchorEl(null);
|
setAnchorEl(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onContextMenuClick = (event) => {
|
const onContextMenuClick = (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
event.nativeEvent.stopImmediatePropagation();
|
event.nativeEvent.stopImmediatePropagation();
|
||||||
setAnchorEl(contextButtonRef.current);
|
setAnchorEl(contextButtonRef.current);
|
||||||
};
|
};
|
||||||
|
|
||||||
const createdAt = DateTime.fromMillis(parseInt(flow.createdAt, 10));
|
const createdAt = DateTime.fromMillis(parseInt(flow.createdAt, 10));
|
||||||
const updatedAt = DateTime.fromMillis(parseInt(flow.updatedAt, 10));
|
const updatedAt = DateTime.fromMillis(parseInt(flow.updatedAt, 10));
|
||||||
const isUpdated = updatedAt > createdAt;
|
const isUpdated = updatedAt > createdAt;
|
||||||
const relativeCreatedAt = createdAt.toRelative();
|
const relativeCreatedAt = createdAt.toRelative();
|
||||||
const relativeUpdatedAt = updatedAt.toRelative();
|
const relativeUpdatedAt = updatedAt.toRelative();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Card sx={{ mb: 1 }} data-test="flow-row">
|
<Card sx={{ mb: 1 }} data-test="flow-row">
|
||||||
@@ -127,7 +131,7 @@ FlowRow.propTypes = {
|
|||||||
flow: FlowPropType.isRequired,
|
flow: FlowPropType.isRequired,
|
||||||
onDeleteFlow: PropTypes.func,
|
onDeleteFlow: PropTypes.func,
|
||||||
onDuplicateFlow: PropTypes.func,
|
onDuplicateFlow: PropTypes.func,
|
||||||
appKey: PropTypes.string.isRequired,
|
appKey: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default FlowRow;
|
export default FlowRow;
|
||||||
|
@@ -11,9 +11,6 @@ import IconButton from '@mui/material/IconButton';
|
|||||||
import ErrorIcon from '@mui/icons-material/Error';
|
import ErrorIcon from '@mui/icons-material/Error';
|
||||||
import CircularProgress from '@mui/material/CircularProgress';
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
||||||
import { yupResolver } from '@hookform/resolvers/yup';
|
|
||||||
import * as yup from 'yup';
|
|
||||||
|
|
||||||
import { EditorContext } from 'contexts/Editor';
|
import { EditorContext } from 'contexts/Editor';
|
||||||
import { StepExecutionsProvider } from 'contexts/StepExecutions';
|
import { StepExecutionsProvider } from 'contexts/StepExecutions';
|
||||||
import TestSubstep from 'components/TestSubstep';
|
import TestSubstep from 'components/TestSubstep';
|
||||||
@@ -33,77 +30,18 @@ import {
|
|||||||
Header,
|
Header,
|
||||||
Wrapper,
|
Wrapper,
|
||||||
} from './style';
|
} from './style';
|
||||||
import isEmpty from 'helpers/isEmpty';
|
|
||||||
import { StepPropType } from 'propTypes/propTypes';
|
import { StepPropType } from 'propTypes/propTypes';
|
||||||
import useTriggers from 'hooks/useTriggers';
|
import useTriggers from 'hooks/useTriggers';
|
||||||
import useActions from 'hooks/useActions';
|
import useActions from 'hooks/useActions';
|
||||||
import useTriggerSubsteps from 'hooks/useTriggerSubsteps';
|
import useTriggerSubsteps from 'hooks/useTriggerSubsteps';
|
||||||
import useActionSubsteps from 'hooks/useActionSubsteps';
|
import useActionSubsteps from 'hooks/useActionSubsteps';
|
||||||
import useStepWithTestExecutions from 'hooks/useStepWithTestExecutions';
|
import useStepWithTestExecutions from 'hooks/useStepWithTestExecutions';
|
||||||
|
import { validationSchemaResolver } from './validation';
|
||||||
|
import { isEqual } from 'lodash';
|
||||||
|
|
||||||
const validIcon = <CheckCircleIcon color="success" />;
|
const validIcon = <CheckCircleIcon color="success" />;
|
||||||
const errorIcon = <ErrorIcon color="error" />;
|
const errorIcon = <ErrorIcon color="error" />;
|
||||||
|
|
||||||
function generateValidationSchema(substeps) {
|
|
||||||
const fieldValidations = substeps?.reduce(
|
|
||||||
(allValidations, { arguments: args }) => {
|
|
||||||
if (!args || !Array.isArray(args)) return allValidations;
|
|
||||||
const substepArgumentValidations = {};
|
|
||||||
for (const arg of args) {
|
|
||||||
const { key, required } = arg;
|
|
||||||
// base validation for the field if not exists
|
|
||||||
if (!substepArgumentValidations[key]) {
|
|
||||||
substepArgumentValidations[key] = yup.mixed();
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
typeof substepArgumentValidations[key] === 'object' &&
|
|
||||||
(arg.type === 'string' || arg.type === 'dropdown')
|
|
||||||
) {
|
|
||||||
// if the field is required, add the required validation
|
|
||||||
if (required) {
|
|
||||||
substepArgumentValidations[key] = substepArgumentValidations[key]
|
|
||||||
.required(`${key} is required.`)
|
|
||||||
.test(
|
|
||||||
'empty-check',
|
|
||||||
`${key} must be not empty`,
|
|
||||||
(value) => !isEmpty(value),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// if the field depends on another field, add the dependsOn required validation
|
|
||||||
if (Array.isArray(arg.dependsOn) && arg.dependsOn.length > 0) {
|
|
||||||
for (const dependsOnKey of arg.dependsOn) {
|
|
||||||
const missingDependencyValueMessage = `We're having trouble loading '${key}' data as required field '${dependsOnKey}' is missing.`;
|
|
||||||
// TODO: make `dependsOnKey` agnostic to the field. However, nested validation schema is not supported.
|
|
||||||
// So the fields under the `parameters` key are subject to their siblings only and thus, `parameters.` is removed.
|
|
||||||
substepArgumentValidations[key] = substepArgumentValidations[
|
|
||||||
key
|
|
||||||
].when(`${dependsOnKey.replace('parameters.', '')}`, {
|
|
||||||
is: (value) => Boolean(value) === false,
|
|
||||||
then: (schema) =>
|
|
||||||
schema
|
|
||||||
.notOneOf([''], missingDependencyValueMessage)
|
|
||||||
.required(missingDependencyValueMessage),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...allValidations,
|
|
||||||
...substepArgumentValidations,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
|
|
||||||
const validationSchema = yup.object({
|
|
||||||
parameters: yup.object(fieldValidations),
|
|
||||||
});
|
|
||||||
|
|
||||||
return yupResolver(validationSchema);
|
|
||||||
}
|
|
||||||
|
|
||||||
function FlowStep(props) {
|
function FlowStep(props) {
|
||||||
const { collapsed, onChange, onContinue, flowId } = props;
|
const { collapsed, onChange, onContinue, flowId } = props;
|
||||||
const editorContext = React.useContext(EditorContext);
|
const editorContext = React.useContext(EditorContext);
|
||||||
@@ -114,6 +52,10 @@ function FlowStep(props) {
|
|||||||
const isAction = step.type === 'action';
|
const isAction = step.type === 'action';
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const [currentSubstep, setCurrentSubstep] = React.useState(0);
|
const [currentSubstep, setCurrentSubstep] = React.useState(0);
|
||||||
|
const [formResolverContext, setFormResolverContext] = React.useState({
|
||||||
|
substeps: [],
|
||||||
|
additionalFields: {},
|
||||||
|
});
|
||||||
const useAppsOptions = {};
|
const useAppsOptions = {};
|
||||||
|
|
||||||
if (isTrigger) {
|
if (isTrigger) {
|
||||||
@@ -168,6 +110,12 @@ function FlowStep(props) {
|
|||||||
? triggerSubstepsData
|
? triggerSubstepsData
|
||||||
: actionSubstepsData || [];
|
: actionSubstepsData || [];
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!isEqual(substeps, formResolverContext.substeps)) {
|
||||||
|
setFormResolverContext({ substeps, additionalFields: {} });
|
||||||
|
}
|
||||||
|
}, [substeps]);
|
||||||
|
|
||||||
const handleChange = React.useCallback(({ step }) => {
|
const handleChange = React.useCallback(({ step }) => {
|
||||||
onChange(step);
|
onChange(step);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -180,11 +128,6 @@ function FlowStep(props) {
|
|||||||
handleChange({ step: val });
|
handleChange({ step: val });
|
||||||
};
|
};
|
||||||
|
|
||||||
const stepValidationSchema = React.useMemo(
|
|
||||||
() => generateValidationSchema(substeps),
|
|
||||||
[substeps],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!apps?.data) {
|
if (!apps?.data) {
|
||||||
return (
|
return (
|
||||||
<CircularProgress
|
<CircularProgress
|
||||||
@@ -213,6 +156,15 @@ function FlowStep(props) {
|
|||||||
value !== substepIndex ? substepIndex : null,
|
value !== substepIndex ? substepIndex : null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const addAdditionalFieldsValidation = (additionalFields) => {
|
||||||
|
if (additionalFields) {
|
||||||
|
setFormResolverContext((prev) => ({
|
||||||
|
...prev,
|
||||||
|
additionalFields: { ...prev.additionalFields, ...additionalFields },
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const validationStatusIcon =
|
const validationStatusIcon =
|
||||||
step.status === 'completed' ? validIcon : errorIcon;
|
step.status === 'completed' ? validIcon : errorIcon;
|
||||||
|
|
||||||
@@ -266,7 +218,8 @@ function FlowStep(props) {
|
|||||||
<Form
|
<Form
|
||||||
defaultValues={step}
|
defaultValues={step}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
resolver={stepValidationSchema}
|
resolver={validationSchemaResolver}
|
||||||
|
context={formResolverContext}
|
||||||
>
|
>
|
||||||
<ChooseAppAndEventSubstep
|
<ChooseAppAndEventSubstep
|
||||||
expanded={currentSubstep === 0}
|
expanded={currentSubstep === 0}
|
||||||
@@ -330,6 +283,9 @@ function FlowStep(props) {
|
|||||||
onSubmit={expandNextStep}
|
onSubmit={expandNextStep}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
step={step}
|
step={step}
|
||||||
|
addAdditionalFieldsValidation={
|
||||||
|
addAdditionalFieldsValidation
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
@@ -360,7 +316,6 @@ function FlowStep(props) {
|
|||||||
FlowStep.propTypes = {
|
FlowStep.propTypes = {
|
||||||
collapsed: PropTypes.bool,
|
collapsed: PropTypes.bool,
|
||||||
step: StepPropType.isRequired,
|
step: StepPropType.isRequired,
|
||||||
index: PropTypes.number,
|
|
||||||
onOpen: PropTypes.func,
|
onOpen: PropTypes.func,
|
||||||
onClose: PropTypes.func,
|
onClose: PropTypes.func,
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
|
120
packages/web/src/components/FlowStep/validation.js
Normal file
120
packages/web/src/components/FlowStep/validation.js
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import * as yup from 'yup';
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
|
import isEmpty from 'helpers/isEmpty';
|
||||||
|
|
||||||
|
function addRequiredValidation({ required, schema, key }) {
|
||||||
|
// if the field is required, add the required validation
|
||||||
|
if (required) {
|
||||||
|
return schema
|
||||||
|
.required(`${key} is required.`)
|
||||||
|
.test(
|
||||||
|
'empty-check',
|
||||||
|
`${key} must be not empty`,
|
||||||
|
(value) => !isEmpty(value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addDependsOnValidation({ schema, dependsOn, key, args }) {
|
||||||
|
// if the field depends on another field, add the dependsOn required validation
|
||||||
|
if (Array.isArray(dependsOn) && dependsOn.length > 0) {
|
||||||
|
for (const dependsOnKey of dependsOn) {
|
||||||
|
const dependsOnKeyShort = dependsOnKey.replace('parameters.', '');
|
||||||
|
const dependsOnField = args.find(({ key }) => key === dependsOnKeyShort);
|
||||||
|
|
||||||
|
if (dependsOnField?.required) {
|
||||||
|
const missingDependencyValueMessage = `We're having trouble loading '${key}' data as required field '${dependsOnKey}' is missing.`;
|
||||||
|
|
||||||
|
// TODO: make `dependsOnKey` agnostic to the field. However, nested validation schema is not supported.
|
||||||
|
// So the fields under the `parameters` key are subject to their siblings only and thus, `parameters.` is removed.
|
||||||
|
return schema.when(dependsOnKeyShort, {
|
||||||
|
is: (dependsOnValue) => Boolean(dependsOnValue) === false,
|
||||||
|
then: (schema) =>
|
||||||
|
schema
|
||||||
|
.notOneOf([''], missingDependencyValueMessage)
|
||||||
|
.required(missingDependencyValueMessage),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validationSchemaResolver(data, context, options) {
|
||||||
|
const { substeps = [], additionalFields = {} } = context;
|
||||||
|
|
||||||
|
const fieldValidations = [
|
||||||
|
...substeps,
|
||||||
|
{
|
||||||
|
arguments: Object.values(additionalFields)
|
||||||
|
.filter((field) => !!field)
|
||||||
|
.flat(),
|
||||||
|
},
|
||||||
|
].reduce((allValidations, { arguments: args }) => {
|
||||||
|
if (!args || !Array.isArray(args)) return allValidations;
|
||||||
|
|
||||||
|
const substepArgumentValidations = {};
|
||||||
|
|
||||||
|
for (const arg of args) {
|
||||||
|
const { key, required } = arg;
|
||||||
|
|
||||||
|
// base validation for the field if not exists
|
||||||
|
if (!substepArgumentValidations[key]) {
|
||||||
|
substepArgumentValidations[key] = yup.mixed();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg.type === 'dynamic') {
|
||||||
|
const fieldsSchema = {};
|
||||||
|
|
||||||
|
for (const field of arg.fields) {
|
||||||
|
fieldsSchema[field.key] = yup.mixed();
|
||||||
|
|
||||||
|
fieldsSchema[field.key] = addRequiredValidation({
|
||||||
|
required: field.required,
|
||||||
|
schema: fieldsSchema[field.key],
|
||||||
|
key: field.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
fieldsSchema[field.key] = addDependsOnValidation({
|
||||||
|
schema: fieldsSchema[field.key],
|
||||||
|
dependsOn: field.dependsOn,
|
||||||
|
key: field.key,
|
||||||
|
args,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
substepArgumentValidations[key] = yup
|
||||||
|
.array()
|
||||||
|
.of(yup.object(fieldsSchema));
|
||||||
|
} else if (
|
||||||
|
typeof substepArgumentValidations[key] === 'object' &&
|
||||||
|
(arg.type === 'string' || arg.type === 'dropdown')
|
||||||
|
) {
|
||||||
|
substepArgumentValidations[key] = addRequiredValidation({
|
||||||
|
required,
|
||||||
|
schema: substepArgumentValidations[key],
|
||||||
|
key,
|
||||||
|
});
|
||||||
|
|
||||||
|
substepArgumentValidations[key] = addDependsOnValidation({
|
||||||
|
schema: substepArgumentValidations[key],
|
||||||
|
dependsOn: arg.dependsOn,
|
||||||
|
key,
|
||||||
|
args,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...allValidations,
|
||||||
|
...substepArgumentValidations,
|
||||||
|
};
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const validationSchema = yup.object({
|
||||||
|
parameters: yup.object(fieldValidations),
|
||||||
|
});
|
||||||
|
|
||||||
|
return yupResolver(validationSchema)(data, context, options);
|
||||||
|
}
|
@@ -43,7 +43,10 @@ function FlowStepContextMenu(props) {
|
|||||||
FlowStepContextMenu.propTypes = {
|
FlowStepContextMenu.propTypes = {
|
||||||
stepId: PropTypes.string.isRequired,
|
stepId: PropTypes.string.isRequired,
|
||||||
onClose: PropTypes.func.isRequired,
|
onClose: PropTypes.func.isRequired,
|
||||||
anchorEl: PropTypes.element.isRequired,
|
anchorEl: PropTypes.oneOfType([
|
||||||
|
PropTypes.func,
|
||||||
|
PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
|
||||||
|
]).isRequired,
|
||||||
deletable: PropTypes.bool.isRequired,
|
deletable: PropTypes.bool.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -19,7 +19,9 @@ function FlowSubstep(props) {
|
|||||||
onCollapse,
|
onCollapse,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
step,
|
step,
|
||||||
|
addAdditionalFieldsValidation,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const { name, arguments: args } = substep;
|
const { name, arguments: args } = substep;
|
||||||
const editorContext = React.useContext(EditorContext);
|
const editorContext = React.useContext(EditorContext);
|
||||||
const formContext = useFormContext();
|
const formContext = useFormContext();
|
||||||
@@ -54,6 +56,7 @@ function FlowSubstep(props) {
|
|||||||
stepId={step.id}
|
stepId={step.id}
|
||||||
disabled={editorContext.readOnly}
|
disabled={editorContext.readOnly}
|
||||||
showOptionValue={true}
|
showOptionValue={true}
|
||||||
|
addAdditionalFieldsValidation={addAdditionalFieldsValidation}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { FormProvider, useForm, useWatch } from 'react-hook-form';
|
import { FormProvider, useForm, useWatch } from 'react-hook-form';
|
||||||
|
|
||||||
const noop = () => null;
|
const noop = () => null;
|
||||||
|
|
||||||
export default function Form(props) {
|
export default function Form(props) {
|
||||||
const {
|
const {
|
||||||
children,
|
children,
|
||||||
@@ -9,24 +11,31 @@ export default function Form(props) {
|
|||||||
resolver,
|
resolver,
|
||||||
render,
|
render,
|
||||||
mode = 'all',
|
mode = 'all',
|
||||||
|
context,
|
||||||
...formProps
|
...formProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const methods = useForm({
|
const methods = useForm({
|
||||||
defaultValues,
|
defaultValues,
|
||||||
reValidateMode: 'onBlur',
|
reValidateMode: 'onBlur',
|
||||||
resolver,
|
resolver,
|
||||||
mode,
|
mode,
|
||||||
|
context,
|
||||||
});
|
});
|
||||||
|
|
||||||
const form = useWatch({ control: methods.control });
|
const form = useWatch({ control: methods.control });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For fields having `dependsOn` fields, we need to re-validate the form.
|
* For fields having `dependsOn` fields, we need to re-validate the form.
|
||||||
*/
|
*/
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
methods.trigger();
|
methods.trigger();
|
||||||
}, [methods.trigger, form]);
|
}, [methods.trigger, form]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
methods.reset(defaultValues);
|
methods.reset(defaultValues);
|
||||||
}, [defaultValues]);
|
}, [defaultValues]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormProvider {...methods}>
|
<FormProvider {...methods}>
|
||||||
<form onSubmit={methods.handleSubmit(onSubmit)} {...formProps}>
|
<form onSubmit={methods.handleSubmit(onSubmit)} {...formProps}>
|
||||||
|
@@ -23,7 +23,9 @@ export default function InputCreator(props) {
|
|||||||
disabled,
|
disabled,
|
||||||
showOptionValue,
|
showOptionValue,
|
||||||
shouldUnregister,
|
shouldUnregister,
|
||||||
|
addAdditionalFieldsValidation,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
key: name,
|
key: name,
|
||||||
label,
|
label,
|
||||||
@@ -33,6 +35,7 @@ export default function InputCreator(props) {
|
|||||||
description,
|
description,
|
||||||
type,
|
type,
|
||||||
} = schema;
|
} = schema;
|
||||||
|
|
||||||
const { data, loading } = useDynamicData(stepId, schema);
|
const { data, loading } = useDynamicData(stepId, schema);
|
||||||
const { data: additionalFieldsData, isLoading: isDynamicFieldsLoading } =
|
const { data: additionalFieldsData, isLoading: isDynamicFieldsLoading } =
|
||||||
useDynamicFields(stepId, schema);
|
useDynamicFields(stepId, schema);
|
||||||
@@ -40,6 +43,10 @@ export default function InputCreator(props) {
|
|||||||
|
|
||||||
const computedName = namePrefix ? `${namePrefix}.${name}` : name;
|
const computedName = namePrefix ? `${namePrefix}.${name}` : name;
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
addAdditionalFieldsValidation?.({ [name]: additionalFields });
|
||||||
|
}, [additionalFields]);
|
||||||
|
|
||||||
if (type === 'dynamic') {
|
if (type === 'dynamic') {
|
||||||
return (
|
return (
|
||||||
<DynamicField
|
<DynamicField
|
||||||
@@ -80,6 +87,7 @@ export default function InputCreator(props) {
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
showOptionValue={showOptionValue}
|
showOptionValue={showOptionValue}
|
||||||
shouldUnregister={shouldUnregister}
|
shouldUnregister={shouldUnregister}
|
||||||
|
componentsProps={{ popper: { className: 'nowheel' } }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@@ -5,8 +5,10 @@ import AddCircleIcon from '@mui/icons-material/AddCircle';
|
|||||||
import CardActionArea from '@mui/material/CardActionArea';
|
import CardActionArea from '@mui/material/CardActionArea';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import { CardContent } from './style';
|
import { CardContent } from './style';
|
||||||
|
|
||||||
export default function NoResultFound(props) {
|
export default function NoResultFound(props) {
|
||||||
const { text, to } = props;
|
const { text, to } = props;
|
||||||
|
|
||||||
const ActionAreaLink = React.useMemo(
|
const ActionAreaLink = React.useMemo(
|
||||||
() =>
|
() =>
|
||||||
React.forwardRef(function InlineLink(linkProps, ref) {
|
React.forwardRef(function InlineLink(linkProps, ref) {
|
||||||
@@ -15,12 +17,12 @@ export default function NoResultFound(props) {
|
|||||||
}),
|
}),
|
||||||
[to],
|
[to],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card elevation={0}>
|
<Card elevation={0}>
|
||||||
<CardActionArea component={ActionAreaLink} {...props}>
|
<CardActionArea component={ActionAreaLink} {...props}>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{!!to && <AddCircleIcon color="primary" />}
|
{!!to && <AddCircleIcon color="primary" />}
|
||||||
|
|
||||||
<Typography variant="body1">{text}</Typography>
|
<Typography variant="body1">{text}</Typography>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</CardActionArea>
|
</CardActionArea>
|
||||||
|
@@ -17,6 +17,7 @@ import { StepExecutionsContext } from 'contexts/StepExecutions';
|
|||||||
import Popper from './Popper';
|
import Popper from './Popper';
|
||||||
import { processStepWithExecutions } from './data';
|
import { processStepWithExecutions } from './data';
|
||||||
import { ChildrenWrapper, FakeInput, InputLabelWrapper } from './style';
|
import { ChildrenWrapper, FakeInput, InputLabelWrapper } from './style';
|
||||||
|
|
||||||
const PowerInput = (props) => {
|
const PowerInput = (props) => {
|
||||||
const { control } = useFormContext();
|
const { control } = useFormContext();
|
||||||
const {
|
const {
|
||||||
@@ -31,33 +32,41 @@ const PowerInput = (props) => {
|
|||||||
} = props;
|
} = props;
|
||||||
const priorStepsWithExecutions = React.useContext(StepExecutionsContext);
|
const priorStepsWithExecutions = React.useContext(StepExecutionsContext);
|
||||||
const editorRef = React.useRef(null);
|
const editorRef = React.useRef(null);
|
||||||
|
|
||||||
const renderElement = React.useCallback(
|
const renderElement = React.useCallback(
|
||||||
(props) => <Element {...props} />,
|
(props) => <Element {...props} />,
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
const [editor] = React.useState(() => customizeEditor(createEditor()));
|
const [editor] = React.useState(() => customizeEditor(createEditor()));
|
||||||
|
|
||||||
const [showVariableSuggestions, setShowVariableSuggestions] =
|
const [showVariableSuggestions, setShowVariableSuggestions] =
|
||||||
React.useState(false);
|
React.useState(false);
|
||||||
|
|
||||||
const disappearSuggestionsOnShift = (event) => {
|
const disappearSuggestionsOnShift = (event) => {
|
||||||
if (event.code === 'Tab') {
|
if (event.code === 'Tab') {
|
||||||
setShowVariableSuggestions(false);
|
setShowVariableSuggestions(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const stepsWithVariables = React.useMemo(() => {
|
const stepsWithVariables = React.useMemo(() => {
|
||||||
return processStepWithExecutions(priorStepsWithExecutions);
|
return processStepWithExecutions(priorStepsWithExecutions);
|
||||||
}, [priorStepsWithExecutions]);
|
}, [priorStepsWithExecutions]);
|
||||||
|
|
||||||
const handleBlur = React.useCallback(
|
const handleBlur = React.useCallback(
|
||||||
(value) => {
|
(value) => {
|
||||||
onBlur?.(value);
|
onBlur?.(value);
|
||||||
},
|
},
|
||||||
[onBlur],
|
[onBlur],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleVariableSuggestionClick = React.useCallback(
|
const handleVariableSuggestionClick = React.useCallback(
|
||||||
(variable) => {
|
(variable) => {
|
||||||
insertVariable(editor, variable, stepsWithVariables);
|
insertVariable(editor, variable, stepsWithVariables);
|
||||||
},
|
},
|
||||||
[stepsWithVariables],
|
[stepsWithVariables],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Controller
|
<Controller
|
||||||
rules={{ required }}
|
rules={{ required }}
|
||||||
@@ -127,6 +136,7 @@ const PowerInput = (props) => {
|
|||||||
anchorEl={editorRef.current}
|
anchorEl={editorRef.current}
|
||||||
data={stepsWithVariables}
|
data={stepsWithVariables}
|
||||||
onSuggestionClick={handleVariableSuggestionClick}
|
onSuggestionClick={handleVariableSuggestionClick}
|
||||||
|
className="nowheel"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormHelperText variant="outlined">{description}</FormHelperText>
|
<FormHelperText variant="outlined">{description}</FormHelperText>
|
||||||
|
@@ -7,6 +7,7 @@ import { useQuery } from '@tanstack/react-query';
|
|||||||
import api from 'helpers/api';
|
import api from 'helpers/api';
|
||||||
|
|
||||||
const variableRegExp = /({.*?})/;
|
const variableRegExp = /({.*?})/;
|
||||||
|
|
||||||
// TODO: extract this function to a separate file
|
// TODO: extract this function to a separate file
|
||||||
function computeArguments(args, getValues) {
|
function computeArguments(args, getValues) {
|
||||||
const initialValue = {};
|
const initialValue = {};
|
||||||
|
9
packages/web/src/hooks/usePrevious.js
Normal file
9
packages/web/src/hooks/usePrevious.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
export const usePrevious = (value) => {
|
||||||
|
const ref = useRef();
|
||||||
|
useEffect(() => {
|
||||||
|
ref.current = value;
|
||||||
|
});
|
||||||
|
return ref.current;
|
||||||
|
};
|
@@ -1,4 +1,6 @@
|
|||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
|
import { Settings } from 'luxon';
|
||||||
|
|
||||||
import ThemeProvider from 'components/ThemeProvider';
|
import ThemeProvider from 'components/ThemeProvider';
|
||||||
import IntlProvider from 'components/IntlProvider';
|
import IntlProvider from 'components/IntlProvider';
|
||||||
import ApolloProvider from 'components/ApolloProvider';
|
import ApolloProvider from 'components/ApolloProvider';
|
||||||
@@ -10,6 +12,9 @@ import Router from 'components/Router';
|
|||||||
import routes from 'routes';
|
import routes from 'routes';
|
||||||
import reportWebVitals from './reportWebVitals';
|
import reportWebVitals from './reportWebVitals';
|
||||||
|
|
||||||
|
// Sets the default locale to English for all luxon DateTime instances created afterwards.
|
||||||
|
Settings.defaultLocale = 'en';
|
||||||
|
|
||||||
const container = document.getElementById('root');
|
const container = document.getElementById('root');
|
||||||
const root = createRoot(container);
|
const root = createRoot(container);
|
||||||
|
|
||||||
|
@@ -30,6 +30,7 @@ import AppIcon from 'components/AppIcon';
|
|||||||
import Container from 'components/Container';
|
import Container from 'components/Container';
|
||||||
import PageTitle from 'components/PageTitle';
|
import PageTitle from 'components/PageTitle';
|
||||||
import useApp from 'hooks/useApp';
|
import useApp from 'hooks/useApp';
|
||||||
|
import Can from 'components/Can';
|
||||||
|
|
||||||
const ReconnectConnection = (props) => {
|
const ReconnectConnection = (props) => {
|
||||||
const { application, onClose } = props;
|
const { application, onClose } = props;
|
||||||
@@ -92,7 +93,7 @@ export default function Application() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
}, [appKey, appConfig?.data, currentUserAbility]);
|
}, [appKey, appConfig?.data, currentUserAbility, formatMessage]);
|
||||||
|
|
||||||
if (loading) return null;
|
if (loading) return null;
|
||||||
|
|
||||||
@@ -118,37 +119,46 @@ export default function Application() {
|
|||||||
<Route
|
<Route
|
||||||
path={`${URLS.FLOWS}/*`}
|
path={`${URLS.FLOWS}/*`}
|
||||||
element={
|
element={
|
||||||
<ConditionalIconButton
|
<Can I="create" a="Flow" passThrough>
|
||||||
type="submit"
|
{(allowed) => (
|
||||||
variant="contained"
|
<ConditionalIconButton
|
||||||
color="primary"
|
type="submit"
|
||||||
size="large"
|
variant="contained"
|
||||||
component={Link}
|
color="primary"
|
||||||
to={URLS.CREATE_FLOW_WITH_APP_AND_CONNECTION(
|
size="large"
|
||||||
appKey,
|
component={Link}
|
||||||
connectionId,
|
to={URLS.CREATE_FLOW_WITH_APP_AND_CONNECTION(
|
||||||
|
appKey,
|
||||||
|
connectionId,
|
||||||
|
)}
|
||||||
|
fullWidth
|
||||||
|
icon={<AddIcon />}
|
||||||
|
disabled={!allowed}
|
||||||
|
>
|
||||||
|
{formatMessage('app.createFlow')}
|
||||||
|
</ConditionalIconButton>
|
||||||
)}
|
)}
|
||||||
fullWidth
|
</Can>
|
||||||
icon={<AddIcon />}
|
|
||||||
disabled={!currentUserAbility.can('create', 'Flow')}
|
|
||||||
>
|
|
||||||
{formatMessage('app.createFlow')}
|
|
||||||
</ConditionalIconButton>
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path={`${URLS.CONNECTIONS}/*`}
|
path={`${URLS.CONNECTIONS}/*`}
|
||||||
element={
|
element={
|
||||||
<SplitButton
|
<Can I="create" a="Connection" passThrough>
|
||||||
disabled={
|
{(allowed) => (
|
||||||
(appConfig?.data &&
|
<SplitButton
|
||||||
!appConfig?.data?.canConnect &&
|
disabled={
|
||||||
!appConfig?.data?.canCustomConnect) ||
|
!allowed ||
|
||||||
connectionOptions.every(({ disabled }) => disabled)
|
(appConfig?.data &&
|
||||||
}
|
!appConfig?.data?.canConnect &&
|
||||||
options={connectionOptions}
|
!appConfig?.data?.canCustomConnect) ||
|
||||||
/>
|
connectionOptions.every(({ disabled }) => disabled)
|
||||||
|
}
|
||||||
|
options={connectionOptions}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Can>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Routes>
|
</Routes>
|
||||||
@@ -169,17 +179,20 @@ export default function Application() {
|
|||||||
label={formatMessage('app.connections')}
|
label={formatMessage('app.connections')}
|
||||||
to={URLS.APP_CONNECTIONS(appKey)}
|
to={URLS.APP_CONNECTIONS(appKey)}
|
||||||
value={URLS.APP_CONNECTIONS_PATTERN}
|
value={URLS.APP_CONNECTIONS_PATTERN}
|
||||||
disabled={!app.supportsConnections}
|
disabled={
|
||||||
|
!currentUserAbility.can('read', 'Connection') ||
|
||||||
|
!app.supportsConnections
|
||||||
|
}
|
||||||
component={Link}
|
component={Link}
|
||||||
data-test="connections-tab"
|
data-test="connections-tab"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Tab
|
<Tab
|
||||||
label={formatMessage('app.flows')}
|
label={formatMessage('app.flows')}
|
||||||
to={URLS.APP_FLOWS(appKey)}
|
to={URLS.APP_FLOWS(appKey)}
|
||||||
value={URLS.APP_FLOWS_PATTERN}
|
value={URLS.APP_FLOWS_PATTERN}
|
||||||
component={Link}
|
component={Link}
|
||||||
data-test="flows-tab"
|
data-test="flows-tab"
|
||||||
|
disabled={!currentUserAbility.can('read', 'Flow')}
|
||||||
/>
|
/>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -187,14 +200,20 @@ export default function Application() {
|
|||||||
<Routes>
|
<Routes>
|
||||||
<Route
|
<Route
|
||||||
path={`${URLS.FLOWS}/*`}
|
path={`${URLS.FLOWS}/*`}
|
||||||
element={<AppFlows appKey={appKey} />}
|
element={
|
||||||
|
<Can I="read" a="Flow">
|
||||||
|
<AppFlows appKey={appKey} />
|
||||||
|
</Can>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path={`${URLS.CONNECTIONS}/*`}
|
path={`${URLS.CONNECTIONS}/*`}
|
||||||
element={<AppConnections appKey={appKey} />}
|
element={
|
||||||
|
<Can I="read" a="Connection">
|
||||||
|
<AppConnections appKey={appKey} />
|
||||||
|
</Can>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/"
|
path="/"
|
||||||
element={
|
element={
|
||||||
@@ -218,17 +237,24 @@ export default function Application() {
|
|||||||
<Route
|
<Route
|
||||||
path="/connections/add"
|
path="/connections/add"
|
||||||
element={
|
element={
|
||||||
<AddAppConnection onClose={goToApplicationPage} application={app} />
|
<Can I="create" a="Connection">
|
||||||
|
<AddAppConnection
|
||||||
|
onClose={goToApplicationPage}
|
||||||
|
application={app}
|
||||||
|
/>
|
||||||
|
</Can>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/connections/:connectionId/reconnect"
|
path="/connections/:connectionId/reconnect"
|
||||||
element={
|
element={
|
||||||
<ReconnectConnection
|
<Can I="create" a="Connection">
|
||||||
application={app}
|
<ReconnectConnection
|
||||||
onClose={goToApplicationPage}
|
application={app}
|
||||||
/>
|
onClose={goToApplicationPage}
|
||||||
|
/>
|
||||||
|
</Can>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
@@ -84,10 +84,14 @@ export default function Applications() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{!isLoading && !hasApps && (
|
{!isLoading && !hasApps && (
|
||||||
<NoResultFound
|
<Can I="create" a="Connection" passThrough>
|
||||||
text={formatMessage('apps.noConnections')}
|
{(allowed) => (
|
||||||
to={URLS.NEW_APP_CONNECTION}
|
<NoResultFound
|
||||||
/>
|
text={formatMessage('apps.noConnections')}
|
||||||
|
{...(allowed && { to: URLS.NEW_APP_CONNECTION })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Can>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isLoading &&
|
{!isLoading &&
|
||||||
|
@@ -7,13 +7,15 @@ import * as URLS from 'config/urls';
|
|||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
import { CREATE_FLOW } from 'graphql/mutations/create-flow';
|
import { CREATE_FLOW } from 'graphql/mutations/create-flow';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
|
|
||||||
export default function CreateFlow() {
|
export default function CreateFlow() {
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const [createFlow] = useMutation(CREATE_FLOW);
|
const [createFlow, { error }] = useMutation(CREATE_FLOW);
|
||||||
const appKey = searchParams.get('appKey');
|
const appKey = searchParams.get('appKey');
|
||||||
const connectionId = searchParams.get('connectionId');
|
const connectionId = searchParams.get('connectionId');
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
async function initiate() {
|
async function initiate() {
|
||||||
const variables = {};
|
const variables = {};
|
||||||
@@ -33,6 +35,11 @@ export default function CreateFlow() {
|
|||||||
}
|
}
|
||||||
initiate();
|
initiate();
|
||||||
}, [createFlow, navigate, appKey, connectionId]);
|
}, [createFlow, navigate, appKey, connectionId]);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@@ -45,7 +52,6 @@ export default function CreateFlow() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CircularProgress size={16} thickness={7.5} />
|
<CircularProgress size={16} thickness={7.5} />
|
||||||
|
|
||||||
<Typography variant="body2">
|
<Typography variant="body2">
|
||||||
{formatMessage('createFlow.creating')}
|
{formatMessage('createFlow.creating')}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
@@ -17,6 +17,7 @@ import Container from 'components/Container';
|
|||||||
import PageTitle from 'components/PageTitle';
|
import PageTitle from 'components/PageTitle';
|
||||||
import SearchInput from 'components/SearchInput';
|
import SearchInput from 'components/SearchInput';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
import useCurrentUserAbility from 'hooks/useCurrentUserAbility';
|
||||||
import * as URLS from 'config/urls';
|
import * as URLS from 'config/urls';
|
||||||
import useLazyFlows from 'hooks/useLazyFlows';
|
import useLazyFlows from 'hooks/useLazyFlows';
|
||||||
|
|
||||||
@@ -26,6 +27,7 @@ export default function Flows() {
|
|||||||
const page = parseInt(searchParams.get('page') || '', 10) || 1;
|
const page = parseInt(searchParams.get('page') || '', 10) || 1;
|
||||||
const [flowName, setFlowName] = React.useState('');
|
const [flowName, setFlowName] = React.useState('');
|
||||||
const [isLoading, setIsLoading] = React.useState(false);
|
const [isLoading, setIsLoading] = React.useState(false);
|
||||||
|
const currentUserAbility = useCurrentUserAbility();
|
||||||
|
|
||||||
const { data, mutate: fetchFlows } = useLazyFlows(
|
const { data, mutate: fetchFlows } = useLazyFlows(
|
||||||
{ flowName, page },
|
{ flowName, page },
|
||||||
@@ -124,7 +126,9 @@ export default function Flows() {
|
|||||||
{!isLoading && !hasFlows && (
|
{!isLoading && !hasFlows && (
|
||||||
<NoResultFound
|
<NoResultFound
|
||||||
text={formatMessage('flows.noFlows')}
|
text={formatMessage('flows.noFlows')}
|
||||||
to={URLS.CREATE_FLOW}
|
{...(currentUserAbility.can('create', 'Flow') && {
|
||||||
|
to: URLS.CREATE_FLOW,
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!isLoading && pageInfo && pageInfo.totalPages > 1 && (
|
{!isLoading && pageInfo && pageInfo.totalPages > 1 && (
|
||||||
|
@@ -3,7 +3,7 @@ services:
|
|||||||
name: automatisch-main
|
name: automatisch-main
|
||||||
env: docker
|
env: docker
|
||||||
dockerfilePath: ./docker/Dockerfile
|
dockerfilePath: ./docker/Dockerfile
|
||||||
dockerContext: ./docker
|
dockerContext: .
|
||||||
repo: https://github.com/automatisch/automatisch
|
repo: https://github.com/automatisch/automatisch
|
||||||
autoDeploy: false
|
autoDeploy: false
|
||||||
envVars:
|
envVars:
|
||||||
@@ -47,7 +47,7 @@ services:
|
|||||||
name: automatisch-worker
|
name: automatisch-worker
|
||||||
env: docker
|
env: docker
|
||||||
dockerfilePath: ./docker/Dockerfile
|
dockerfilePath: ./docker/Dockerfile
|
||||||
dockerContext: ./docker
|
dockerContext: .
|
||||||
repo: https://github.com/automatisch/automatisch
|
repo: https://github.com/automatisch/automatisch
|
||||||
autoDeploy: false
|
autoDeploy: false
|
||||||
envVars:
|
envVars:
|
||||||
|
384
yarn.lock
384
yarn.lock
@@ -1455,6 +1455,18 @@
|
|||||||
enabled "2.0.x"
|
enabled "2.0.x"
|
||||||
kuler "^2.0.0"
|
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":
|
"@docsearch/css@3.2.1", "@docsearch/css@^3.2.1":
|
||||||
version "3.2.1"
|
version "3.2.1"
|
||||||
resolved "https://registry.npmjs.org/@docsearch/css/-/css-3.2.1.tgz"
|
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"
|
resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz"
|
||||||
integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==
|
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":
|
"@rollup/plugin-babel@^5.2.0":
|
||||||
version "5.3.0"
|
version "5.3.0"
|
||||||
resolved "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.0.tgz"
|
resolved "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.0.tgz"
|
||||||
@@ -3823,6 +3901,216 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@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":
|
"@types/debug@^4.1.7":
|
||||||
version "4.1.8"
|
version "4.1.8"
|
||||||
resolved "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz"
|
resolved "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz"
|
||||||
@@ -3913,6 +4201,11 @@
|
|||||||
"@types/qs" "*"
|
"@types/qs" "*"
|
||||||
"@types/serve-static" "*"
|
"@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":
|
"@types/graceful-fs@^4.1.2":
|
||||||
version "4.1.5"
|
version "4.1.5"
|
||||||
resolved "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz"
|
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"
|
resolved "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz"
|
||||||
integrity sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==
|
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:
|
clean-css@^5.2.2:
|
||||||
version "5.2.2"
|
version "5.2.2"
|
||||||
resolved "https://registry.npmjs.org/clean-css/-/clean-css-5.2.2.tgz"
|
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"
|
resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz"
|
||||||
integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==
|
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:
|
damerau-levenshtein@^1.0.7:
|
||||||
version "1.0.8"
|
version "1.0.8"
|
||||||
resolved "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz"
|
resolved "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz"
|
||||||
@@ -13809,6 +14169,18 @@ react@^18.2.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
loose-envify "^1.1.0"
|
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:
|
read-cmd-shim@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-2.0.0.tgz"
|
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:
|
dependencies:
|
||||||
prepend-http "^2.0.0"
|
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:
|
util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"
|
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"
|
version "0.8.15"
|
||||||
resolved "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz"
|
resolved "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz"
|
||||||
integrity sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==
|
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"
|
||||||
|
Reference in New Issue
Block a user