Compare commits
12 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
96c3c19a50 | ||
![]() |
abfd1116c7 | ||
![]() |
017854955d | ||
![]() |
1405cddea1 | ||
![]() |
00dd3164c9 | ||
![]() |
d5cbc0f611 | ||
![]() |
5d2e9ccc67 | ||
![]() |
017a881494 | ||
![]() |
52994970e6 | ||
![]() |
ebae629e5c | ||
![]() |
4d79220b0c | ||
![]() |
96fba7fbb8 |
@@ -1 +0,0 @@
|
|||||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 222.61 155.27"><defs><style>.cls-1{fill:#00bf6f;}</style></defs><title>Goldie_Sabaeus_RGB</title><path id="_Compound_Path_" data-name="<Compound Path>" class="cls-1" d="M249,141.26a26.52,26.52,0,0,0-6.29.78,81.08,81.08,0,0,0-64.19-59.81c-1.4-.25-2.66-.44-4.09-.62h0c.24-7.66.59-16.51,11.86-24.47l-1.78-4.49s-22,6.82-24.49,25.61c-1.09-5.11-11.33-11.51-16.4-12.72l-2.52,4.07s6.72,3.36,8.36,12.63h0A81.08,81.08,0,0,0,85.24,142a26.3,26.3,0,1,0,3.32,50,80.63,80.63,0,0,0,8.54,15.89l21.83-14.71-.19-.24c-5.77-7.42-9.3-18.34-9.9-29.21-.65-12,2.27-23.9,9.93-30.9,15.79-13.44,33-7.31,43.74,5.57h2.89c10.77-12.88,28-19,43.74-5.57,7.65,7,10.58,18.91,9.93,30.9-.59,10.87-4.12,21.79-9.9,29.21l-.19.24,21.83,14.71A80.63,80.63,0,0,0,239.37,192,26.29,26.29,0,1,0,249,141.26Zm-170.53,34a7.76,7.76,0,0,1,0-15.52,7.83,7.83,0,0,1,4.35,1.34,117.87,117.87,0,0,0,.83,12.19A7.76,7.76,0,0,1,78.44,175.31Zm171,0a7.76,7.76,0,0,1-5.18-2,117.87,117.87,0,0,0,.83-12.19,7.75,7.75,0,0,1,12.1,6.43A7.74,7.74,0,0,1,249.48,175.31Z" transform="translate(-52.66 -52.66)"/></svg>
|
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,24 +0,0 @@
|
|||||||
import crypto from 'crypto';
|
|
||||||
import { URLSearchParams } from 'url';
|
|
||||||
|
|
||||||
export default async function generateAuthUrl($) {
|
|
||||||
const oauthRedirectUrlField = $.app.auth.fields.find(
|
|
||||||
(field) => field.key == 'oAuthRedirectUrl'
|
|
||||||
);
|
|
||||||
const redirectUri = oauthRedirectUrlField.value;
|
|
||||||
const state = crypto.randomBytes(100).toString('base64url');
|
|
||||||
|
|
||||||
const searchParams = new URLSearchParams({
|
|
||||||
client_id: $.auth.data.clientId,
|
|
||||||
redirect_uri: redirectUri,
|
|
||||||
response_type: 'code',
|
|
||||||
state,
|
|
||||||
});
|
|
||||||
|
|
||||||
const url = `https://api.surveymonkey.com/oauth/authorize?${searchParams.toString()}`;
|
|
||||||
|
|
||||||
await $.auth.set({
|
|
||||||
url,
|
|
||||||
originalState: state,
|
|
||||||
});
|
|
||||||
}
|
|
@@ -1,46 +0,0 @@
|
|||||||
import generateAuthUrl from './generate-auth-url.js';
|
|
||||||
import verifyCredentials from './verify-credentials.js';
|
|
||||||
import isStillVerified from './is-still-verified.js';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
key: 'oAuthRedirectUrl',
|
|
||||||
label: 'OAuth Redirect URL',
|
|
||||||
type: 'string',
|
|
||||||
required: true,
|
|
||||||
readOnly: true,
|
|
||||||
value: '{WEB_APP_URL}/app/surveymonkey/connections/add',
|
|
||||||
placeholder: null,
|
|
||||||
description:
|
|
||||||
'When asked to input a redirect URL in SurveyMonkey, enter the URL above.',
|
|
||||||
clickToCopy: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'clientId',
|
|
||||||
label: 'Client ID',
|
|
||||||
type: 'string',
|
|
||||||
required: true,
|
|
||||||
readOnly: false,
|
|
||||||
value: null,
|
|
||||||
placeholder: null,
|
|
||||||
description: null,
|
|
||||||
clickToCopy: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'clientSecret',
|
|
||||||
label: 'Client Secret',
|
|
||||||
type: 'string',
|
|
||||||
required: true,
|
|
||||||
readOnly: false,
|
|
||||||
value: null,
|
|
||||||
placeholder: null,
|
|
||||||
description: null,
|
|
||||||
clickToCopy: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
generateAuthUrl,
|
|
||||||
verifyCredentials,
|
|
||||||
isStillVerified,
|
|
||||||
};
|
|
@@ -1,8 +0,0 @@
|
|||||||
import getCurrentUser from '../common/get-current-user.js';
|
|
||||||
|
|
||||||
const isStillVerified = async ($) => {
|
|
||||||
const currentUser = await getCurrentUser($);
|
|
||||||
return !!currentUser.id;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default isStillVerified;
|
|
@@ -1,48 +0,0 @@
|
|||||||
import getCurrentUser from '../common/get-current-user.js';
|
|
||||||
|
|
||||||
const verifyCredentials = async ($) => {
|
|
||||||
if ($.auth.data.originalState !== $.auth.data.state) {
|
|
||||||
throw new Error("The 'state' parameter does not match.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const oauthRedirectUrlField = $.app.auth.fields.find(
|
|
||||||
(field) => field.key == 'oAuthRedirectUrl'
|
|
||||||
);
|
|
||||||
const redirectUri = oauthRedirectUrlField.value;
|
|
||||||
const { data } = await $.http.post(
|
|
||||||
'https://api.surveymonkey.com/oauth/token',
|
|
||||||
{
|
|
||||||
client_secret: $.auth.data.clientSecret,
|
|
||||||
code: $.auth.data.code,
|
|
||||||
redirect_uri: redirectUri,
|
|
||||||
client_id: $.auth.data.clientId,
|
|
||||||
grant_type: 'authorization_code',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
await $.auth.set({
|
|
||||||
accessToken: data.access_token,
|
|
||||||
tokenType: data.token_type,
|
|
||||||
accessUrl: data.access_url,
|
|
||||||
expiresIn: data.expires_in,
|
|
||||||
});
|
|
||||||
|
|
||||||
const currentUser = await getCurrentUser($);
|
|
||||||
|
|
||||||
const screenName = [currentUser.username, currentUser.email]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(' @ ');
|
|
||||||
|
|
||||||
await $.auth.set({
|
|
||||||
clientId: $.auth.data.clientId,
|
|
||||||
clientSecret: $.auth.data.clientSecret,
|
|
||||||
screenName,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export default verifyCredentials;
|
|
@@ -1,9 +0,0 @@
|
|||||||
const addAuthHeader = ($, requestConfig) => {
|
|
||||||
if ($.auth.data?.accessToken) {
|
|
||||||
requestConfig.headers.Authorization = `${$.auth.data.tokenType} ${$.auth.data.accessToken}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return requestConfig;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default addAuthHeader;
|
|
@@ -1,6 +0,0 @@
|
|||||||
const getCurrentUser = async ($) => {
|
|
||||||
const { data: currentUser } = await $.http.get('/v3/users/me');
|
|
||||||
return currentUser;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default getCurrentUser;
|
|
@@ -1,11 +0,0 @@
|
|||||||
const setBaseUrl = ($, requestConfig) => {
|
|
||||||
const accessUrl = $.auth.data.accessUrl;
|
|
||||||
|
|
||||||
if (accessUrl) {
|
|
||||||
requestConfig.baseURL = accessUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
return requestConfig;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default setBaseUrl;
|
|
@@ -1,17 +0,0 @@
|
|||||||
import defineApp from '../../helpers/define-app.js';
|
|
||||||
import addAuthHeader from './common/add-auth-header.js';
|
|
||||||
import auth from './auth/index.js';
|
|
||||||
import setBaseUrl from './common/set-base-url.js';
|
|
||||||
|
|
||||||
export default defineApp({
|
|
||||||
name: 'SurveyMonkey',
|
|
||||||
key: 'surveymonkey',
|
|
||||||
baseUrl: 'https://www.surveymonkey.com',
|
|
||||||
apiBaseUrl: '',
|
|
||||||
iconUrl: '{BASE_URL}/apps/surveymonkey/assets/favicon.svg',
|
|
||||||
authDocUrl: '{DOCS_URL}/apps/surveymonkey/connection',
|
|
||||||
primaryColor: '00bf6f',
|
|
||||||
supportsConnections: true,
|
|
||||||
beforeRequest: [setBaseUrl, addAuthHeader],
|
|
||||||
auth,
|
|
||||||
});
|
|
@@ -437,14 +437,6 @@ export default defineConfig({
|
|||||||
{ text: 'Connection', link: '/apps/stripe/connection' },
|
{ text: 'Connection', link: '/apps/stripe/connection' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
text: 'SurveyMonkey',
|
|
||||||
collapsible: true,
|
|
||||||
collapsed: true,
|
|
||||||
items: [
|
|
||||||
{ text: 'Connection', link: '/apps/surveymonkey/connection' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
text: 'Telegram',
|
text: 'Telegram',
|
||||||
collapsible: true,
|
collapsible: true,
|
||||||
|
@@ -1,18 +0,0 @@
|
|||||||
# SurveyMonkey
|
|
||||||
|
|
||||||
:::info
|
|
||||||
This page explains the steps you need to follow to set up the SurveyMonkey
|
|
||||||
connection in Automatisch. If any of the steps are outdated, please let us know!
|
|
||||||
:::
|
|
||||||
|
|
||||||
1. Go to the [My Apps page of your SurveyMonkey account](https://developer.surveymonkey.com/apps/) to create a project.
|
|
||||||
2. Click on the **Add a New App** button.
|
|
||||||
3. Fill the form and submit it.
|
|
||||||
4. Go to **Settings** page.
|
|
||||||
5. Copy **OAuth Redirect URL** from Automatisch to **OAuth Redirect URIs** field, and click on the **Submit Changes** button.
|
|
||||||
6. In the same page, go to the **Scopes** section.
|
|
||||||
7. Select **Create/Modify Surveys**, **View Surveys**, **View Collectors**, **View Responses**, **Create/Modify Contacts**, **View Contacts**, **Create/Modify Webhooks**, **View Users** and **View Webhooks** scopes and click on the **Update Scopes** button.
|
|
||||||
8. Copy the **Client ID** value in the same page to the `Client ID` field on Automatisch.
|
|
||||||
9. Copy the **Secret** value in the same page to the `Client Secret` field on Automatisch.
|
|
||||||
10. Click **Submit** button on Automatisch.
|
|
||||||
11. Congrats! Start using your new SurveyMonkey connection within the flows.
|
|
@@ -1 +0,0 @@
|
|||||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 222.61 155.27"><defs><style>.cls-1{fill:#00bf6f;}</style></defs><title>Goldie_Sabaeus_RGB</title><path id="_Compound_Path_" data-name="<Compound Path>" class="cls-1" d="M249,141.26a26.52,26.52,0,0,0-6.29.78,81.08,81.08,0,0,0-64.19-59.81c-1.4-.25-2.66-.44-4.09-.62h0c.24-7.66.59-16.51,11.86-24.47l-1.78-4.49s-22,6.82-24.49,25.61c-1.09-5.11-11.33-11.51-16.4-12.72l-2.52,4.07s6.72,3.36,8.36,12.63h0A81.08,81.08,0,0,0,85.24,142a26.3,26.3,0,1,0,3.32,50,80.63,80.63,0,0,0,8.54,15.89l21.83-14.71-.19-.24c-5.77-7.42-9.3-18.34-9.9-29.21-.65-12,2.27-23.9,9.93-30.9,15.79-13.44,33-7.31,43.74,5.57h2.89c10.77-12.88,28-19,43.74-5.57,7.65,7,10.58,18.91,9.93,30.9-.59,10.87-4.12,21.79-9.9,29.21l-.19.24,21.83,14.71A80.63,80.63,0,0,0,239.37,192,26.29,26.29,0,1,0,249,141.26Zm-170.53,34a7.76,7.76,0,0,1,0-15.52,7.83,7.83,0,0,1,4.35,1.34,117.87,117.87,0,0,0,.83,12.19A7.76,7.76,0,0,1,78.44,175.31Zm171,0a7.76,7.76,0,0,1-5.18-2,117.87,117.87,0,0,0,.83-12.19,7.75,7.75,0,0,1,12.1,6.43A7.74,7.74,0,0,1,249.48,175.31Z" transform="translate(-52.66 -52.66)"/></svg>
|
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
@@ -1,79 +0,0 @@
|
|||||||
import { EdgeLabelRenderer, getStraightPath } from 'reactflow';
|
|
||||||
import IconButton from '@mui/material/IconButton';
|
|
||||||
import AddIcon from '@mui/icons-material/Add';
|
|
||||||
import { useMutation } from '@apollo/client';
|
|
||||||
import { CREATE_STEP } from 'graphql/mutations/create-step';
|
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
export default function Edge({
|
|
||||||
sourceX,
|
|
||||||
sourceY,
|
|
||||||
targetX,
|
|
||||||
targetY,
|
|
||||||
source,
|
|
||||||
data: { flowId, setCurrentStepId, flowActive, layouted },
|
|
||||||
}) {
|
|
||||||
const [createStep, { loading: creationInProgress }] =
|
|
||||||
useMutation(CREATE_STEP);
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const [edgePath, labelX, labelY] = getStraightPath({
|
|
||||||
sourceX,
|
|
||||||
sourceY,
|
|
||||||
targetX,
|
|
||||||
targetY,
|
|
||||||
});
|
|
||||||
|
|
||||||
const addStep = async (previousStepId) => {
|
|
||||||
const mutationInput = {
|
|
||||||
previousStep: {
|
|
||||||
id: previousStepId,
|
|
||||||
},
|
|
||||||
flow: {
|
|
||||||
id: flowId,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const createdStep = await createStep({
|
|
||||||
variables: { input: mutationInput },
|
|
||||||
});
|
|
||||||
|
|
||||||
const createdStepId = createdStep.data.createStep.id;
|
|
||||||
setCurrentStepId(createdStepId);
|
|
||||||
await queryClient.invalidateQueries({ queryKey: ['flows', flowId] });
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<EdgeLabelRenderer>
|
|
||||||
<IconButton
|
|
||||||
onClick={() => addStep(source)}
|
|
||||||
color="primary"
|
|
||||||
sx={{
|
|
||||||
position: 'absolute',
|
|
||||||
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
|
|
||||||
pointerEvents: 'all',
|
|
||||||
visibility: layouted ? 'visible' : 'hidden',
|
|
||||||
}}
|
|
||||||
disabled={creationInProgress || 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({
|
|
||||||
flowId: PropTypes.string.isRequired,
|
|
||||||
setCurrentStepId: PropTypes.func.isRequired,
|
|
||||||
flowActive: PropTypes.bool.isRequired,
|
|
||||||
layouted: PropTypes.bool,
|
|
||||||
}).isRequired,
|
|
||||||
};
|
|
@@ -0,0 +1,69 @@
|
|||||||
|
import { EdgeLabelRenderer, getStraightPath, BaseEdge } from 'reactflow';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
|
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { useContext } from 'react';
|
||||||
|
import { EdgesContext } from '../../EditorNew';
|
||||||
|
import { Tooltip } from '@mui/material';
|
||||||
|
|
||||||
|
export default function NodeEdge({
|
||||||
|
sourceX,
|
||||||
|
sourceY,
|
||||||
|
targetX,
|
||||||
|
targetY,
|
||||||
|
source,
|
||||||
|
data: { laidOut },
|
||||||
|
}) {
|
||||||
|
const { stepCreationInProgress, flowActive, onAddStep } =
|
||||||
|
useContext(EdgesContext);
|
||||||
|
|
||||||
|
const [edgePath, labelX, labelY] = getStraightPath({
|
||||||
|
sourceX,
|
||||||
|
sourceY,
|
||||||
|
targetX,
|
||||||
|
targetY,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleAddStep = () => {
|
||||||
|
onAddStep(source);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<BaseEdge path={edgePath} />
|
||||||
|
<EdgeLabelRenderer>
|
||||||
|
<Tooltip title="Add step">
|
||||||
|
<IconButton
|
||||||
|
onClick={handleAddStep}
|
||||||
|
color="primary"
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
|
||||||
|
pointerEvents: 'all',
|
||||||
|
backgroundColor: '#fafafa',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: '#f0f3fa',
|
||||||
|
},
|
||||||
|
// visibility: laidOut ? 'visible' : 'hidden',
|
||||||
|
}}
|
||||||
|
disabled={stepCreationInProgress || flowActive}
|
||||||
|
>
|
||||||
|
<AddIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</EdgeLabelRenderer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
NodeEdge.propTypes = {
|
||||||
|
sourceX: PropTypes.number.isRequired,
|
||||||
|
sourceY: PropTypes.number.isRequired,
|
||||||
|
targetX: PropTypes.number.isRequired,
|
||||||
|
targetY: PropTypes.number.isRequired,
|
||||||
|
source: PropTypes.string.isRequired,
|
||||||
|
data: PropTypes.shape({
|
||||||
|
laidOut: PropTypes.bool,
|
||||||
|
}).isRequired,
|
||||||
|
};
|
@@ -0,0 +1,95 @@
|
|||||||
|
import { EdgeLabelRenderer, getStraightPath, BaseEdge } from 'reactflow';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
|
import Menu from '@mui/material/Menu';
|
||||||
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
|
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { useContext, useState } from 'react';
|
||||||
|
import { EdgesContext } from '../../EditorNew';
|
||||||
|
import { Tooltip } from '@mui/material';
|
||||||
|
|
||||||
|
export default function NodeOrPathsEdge({
|
||||||
|
sourceX,
|
||||||
|
sourceY,
|
||||||
|
targetX,
|
||||||
|
targetY,
|
||||||
|
source,
|
||||||
|
data: { laidOut },
|
||||||
|
}) {
|
||||||
|
const [anchorEl, setAnchorEl] = useState(null);
|
||||||
|
const open = Boolean(anchorEl);
|
||||||
|
const handleClick = (event) => {
|
||||||
|
setAnchorEl(event.currentTarget);
|
||||||
|
};
|
||||||
|
const handleClose = () => {
|
||||||
|
setAnchorEl(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { stepCreationInProgress, flowActive, onAddStep, onAddPaths } =
|
||||||
|
useContext(EdgesContext);
|
||||||
|
|
||||||
|
const [edgePath, labelX, labelY] = getStraightPath({
|
||||||
|
sourceX,
|
||||||
|
sourceY,
|
||||||
|
targetX,
|
||||||
|
targetY,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleAddStep = () => {
|
||||||
|
onAddStep(source);
|
||||||
|
handleClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddPaths = () => {
|
||||||
|
onAddPaths(source);
|
||||||
|
handleClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<BaseEdge path={edgePath} />
|
||||||
|
<EdgeLabelRenderer>
|
||||||
|
<Tooltip title="Add step or paths">
|
||||||
|
<IconButton
|
||||||
|
onClick={handleClick}
|
||||||
|
color="primary"
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
|
||||||
|
pointerEvents: 'all',
|
||||||
|
backgroundColor: '#fafafa',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: '#f0f3fa',
|
||||||
|
},
|
||||||
|
// visibility: laidOut ? 'visible' : 'hidden',
|
||||||
|
}}
|
||||||
|
disabled={stepCreationInProgress || flowActive}
|
||||||
|
>
|
||||||
|
<AddIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Menu
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
open={open}
|
||||||
|
onClose={handleClose}
|
||||||
|
anchorOrigin={{ horizontal: 'right', vertical: 'top' }}
|
||||||
|
>
|
||||||
|
<MenuItem onClick={handleAddStep}>Step</MenuItem>
|
||||||
|
<MenuItem onClick={handleAddPaths}>Paths</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
</EdgeLabelRenderer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
NodeOrPathsEdge.propTypes = {
|
||||||
|
sourceX: PropTypes.number.isRequired,
|
||||||
|
sourceY: PropTypes.number.isRequired,
|
||||||
|
targetX: PropTypes.number.isRequired,
|
||||||
|
targetY: PropTypes.number.isRequired,
|
||||||
|
source: PropTypes.string.isRequired,
|
||||||
|
data: PropTypes.shape({
|
||||||
|
laidOut: PropTypes.bool,
|
||||||
|
}).isRequired,
|
||||||
|
};
|
@@ -0,0 +1,69 @@
|
|||||||
|
import { EdgeLabelRenderer, getStraightPath, BaseEdge } from 'reactflow';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
|
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { useContext } from 'react';
|
||||||
|
import { EdgesContext } from '../../EditorNew';
|
||||||
|
import { Tooltip } from '@mui/material';
|
||||||
|
|
||||||
|
export default function PathsEdge({
|
||||||
|
sourceX,
|
||||||
|
sourceY,
|
||||||
|
targetX,
|
||||||
|
targetY,
|
||||||
|
source,
|
||||||
|
data: { laidOut },
|
||||||
|
}) {
|
||||||
|
const { stepCreationInProgress, flowActive, onAddPath } =
|
||||||
|
useContext(EdgesContext);
|
||||||
|
|
||||||
|
const [edgePath, labelX, labelY] = getStraightPath({
|
||||||
|
sourceX,
|
||||||
|
sourceY,
|
||||||
|
targetX,
|
||||||
|
targetY,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleAddPath = () => {
|
||||||
|
onAddPath(source);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<BaseEdge path={edgePath} />
|
||||||
|
<EdgeLabelRenderer>
|
||||||
|
<Tooltip title="Add path">
|
||||||
|
<IconButton
|
||||||
|
onClick={handleAddPath}
|
||||||
|
color="primary"
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
|
||||||
|
pointerEvents: 'all',
|
||||||
|
backgroundColor: '#fafafa',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: '#f0f3fa',
|
||||||
|
},
|
||||||
|
// visibility: laidOut ? 'visible' : 'hidden',
|
||||||
|
}}
|
||||||
|
disabled={stepCreationInProgress || flowActive}
|
||||||
|
>
|
||||||
|
<AddIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</EdgeLabelRenderer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
PathsEdge.propTypes = {
|
||||||
|
sourceX: PropTypes.number.isRequired,
|
||||||
|
sourceY: PropTypes.number.isRequired,
|
||||||
|
targetX: PropTypes.number.isRequired,
|
||||||
|
targetY: PropTypes.number.isRequired,
|
||||||
|
source: PropTypes.string.isRequired,
|
||||||
|
data: PropTypes.shape({
|
||||||
|
laidOut: PropTypes.bool,
|
||||||
|
}).isRequired,
|
||||||
|
};
|
@@ -1,255 +1,181 @@
|
|||||||
import { useEffect, useState, useCallback } from 'react';
|
import { useEffect, useCallback, createContext, useRef } from 'react';
|
||||||
import { useMutation } from '@apollo/client';
|
// import { useMutation } from '@apollo/client';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
// import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { FlowPropType } from 'propTypes/propTypes';
|
import { FlowPropType } from 'propTypes/propTypes';
|
||||||
import ReactFlow, { useNodesState, useEdgesState, addEdge } from 'reactflow';
|
import ReactFlow, { useNodesState, useEdgesState } from 'reactflow';
|
||||||
import 'reactflow/dist/style.css';
|
import 'reactflow/dist/style.css';
|
||||||
import { UPDATE_STEP } from 'graphql/mutations/update-step';
|
// import { UPDATE_STEP } from 'graphql/mutations/update-step';
|
||||||
|
// import { CREATE_STEP } from 'graphql/mutations/create-step';
|
||||||
|
|
||||||
import { useAutoLayout } from './useAutoLayout';
|
import { useAutoLayout } from './useAutoLayout';
|
||||||
import { useScrollBoundries } from './useScrollBoundries';
|
import NodeOrPathsEdge from './Edges/NodeOrPathsEdge/NodeOrPathsEdge';
|
||||||
import FlowStepNode from './FlowStepNode/FlowStepNode';
|
import FlowStepNode from './Nodes/FlowStepNode/FlowStepNode';
|
||||||
import Edge from './Edge/Edge';
|
import InvisibleNode from './Nodes/InvisibleNode/InvisibleNode';
|
||||||
import InvisibleNode from './InvisibleNode/InvisibleNode';
|
import PathsNode from './Nodes/PathsNode/PathsNode';
|
||||||
import { EditorWrapper } from './style';
|
import { EditorWrapper } from './style';
|
||||||
|
import { generateEdges, generateNodes, updatedCollapsedNodes } from './utils';
|
||||||
|
import { EDGE_TYPES, NODE_TYPES } from './constants';
|
||||||
|
import { useFlow } from './temp/useFlow';
|
||||||
|
import PathNode from './Nodes/PathNode/PathNode';
|
||||||
|
import PathsEdge from './Edges/PathsEdge/PathsEdge';
|
||||||
|
import NodeEdge from './Edges/NodeEdge/NodeEdge';
|
||||||
|
|
||||||
const nodeTypes = { flowStep: FlowStepNode, invisible: InvisibleNode };
|
export const EdgesContext = createContext();
|
||||||
|
export const NodesContext = createContext();
|
||||||
|
|
||||||
|
const nodeTypes = {
|
||||||
|
[NODE_TYPES.FLOW_STEP]: FlowStepNode,
|
||||||
|
[NODE_TYPES.INVISIBLE]: InvisibleNode,
|
||||||
|
[NODE_TYPES.PATHS]: PathsNode,
|
||||||
|
[NODE_TYPES.PATH]: PathNode,
|
||||||
|
};
|
||||||
|
|
||||||
const edgeTypes = {
|
const edgeTypes = {
|
||||||
addNodeEdge: Edge,
|
[EDGE_TYPES.ADD_NODE_OR_PATHS_EDGE]: NodeOrPathsEdge,
|
||||||
|
[EDGE_TYPES.ADD_PATH_EDGE]: PathsEdge,
|
||||||
|
[EDGE_TYPES.ADD_NODE_EDGE]: NodeEdge,
|
||||||
};
|
};
|
||||||
|
|
||||||
const INVISIBLE_NODE_ID = 'invisible-node';
|
const EditorNew = () =>
|
||||||
|
// { flow }
|
||||||
|
{
|
||||||
|
const { flow, createStep, createPaths, createPath } = useFlow();
|
||||||
|
// const [updateStep] = useMutation(UPDATE_STEP);
|
||||||
|
// const queryClient = useQueryClient();
|
||||||
|
// const [createStep, { loading: stepCreationInProgress }] =
|
||||||
|
// useMutation(CREATE_STEP);
|
||||||
|
const stepCreationInProgress = false;
|
||||||
|
|
||||||
const generateEdgeId = (sourceId, targetId) => `${sourceId}-${targetId}`;
|
const [nodes, setNodes, onNodesChange] = useNodesState(
|
||||||
|
generateNodes({ steps: flow.steps }),
|
||||||
const EditorNew = ({ flow }) => {
|
);
|
||||||
const [triggerStep] = flow.steps;
|
const [edges, setEdges, onEdgesChange] = useEdgesState(
|
||||||
const [currentStepId, setCurrentStepId] = useState(triggerStep.id);
|
generateEdges({ steps: flow.steps }),
|
||||||
|
|
||||||
const [updateStep] = useMutation(UPDATE_STEP);
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
|
||||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
|
||||||
useAutoLayout();
|
|
||||||
useScrollBoundries();
|
|
||||||
|
|
||||||
const onConnect = useCallback(
|
|
||||||
(params) => setEdges((eds) => addEdge(params, eds)),
|
|
||||||
[setEdges],
|
|
||||||
);
|
|
||||||
|
|
||||||
const openNextStep = useCallback(
|
|
||||||
(nextStep) => () => {
|
|
||||||
setCurrentStepId(nextStep?.id);
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
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 generateEdges = useCallback((flow, prevEdges) => {
|
|
||||||
const newEdges =
|
|
||||||
flow.steps
|
|
||||||
.map((step, i) => {
|
|
||||||
const sourceId = step.id;
|
|
||||||
const targetId = flow.steps[i + 1]?.id;
|
|
||||||
const edge = prevEdges?.find(
|
|
||||||
(edge) => edge.id === generateEdgeId(sourceId, targetId),
|
|
||||||
);
|
|
||||||
if (targetId) {
|
|
||||||
return {
|
|
||||||
id: generateEdgeId(sourceId, targetId),
|
|
||||||
source: sourceId,
|
|
||||||
target: targetId,
|
|
||||||
type: 'addNodeEdge',
|
|
||||||
data: {
|
|
||||||
flowId: flow.id,
|
|
||||||
flowActive: flow.active,
|
|
||||||
setCurrentStepId,
|
|
||||||
layouted: !!edge,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.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: {
|
|
||||||
flowId: flow.id,
|
|
||||||
flowActive: flow.active,
|
|
||||||
setCurrentStepId,
|
|
||||||
layouted: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: newEdges;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const generateNodes = useCallback(
|
|
||||||
(flow, prevNodes) => {
|
|
||||||
const newNodes = flow.steps.map((step, index) => {
|
|
||||||
const node = prevNodes?.find(({ id }) => id === step.id);
|
|
||||||
const collapsed = currentStepId !== step.id;
|
|
||||||
return {
|
|
||||||
id: step.id,
|
|
||||||
type: 'flowStep',
|
|
||||||
position: {
|
|
||||||
x: node ? node.position.x : 0,
|
|
||||||
y: node ? node.position.y : 0,
|
|
||||||
},
|
|
||||||
zIndex: collapsed ? 0 : 1,
|
|
||||||
data: {
|
|
||||||
step,
|
|
||||||
index: index,
|
|
||||||
flowId: flow.id,
|
|
||||||
collapsed,
|
|
||||||
openNextStep: openNextStep(flow.steps[index + 1]),
|
|
||||||
onOpen: () => setCurrentStepId(step.id),
|
|
||||||
onClose: () => setCurrentStepId(null),
|
|
||||||
onChange: onStepChange,
|
|
||||||
layouted: !!node,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const prevInvisibleNode = nodes.find((node) => node.type === 'invisible');
|
|
||||||
|
|
||||||
return [
|
|
||||||
...newNodes,
|
|
||||||
{
|
|
||||||
id: INVISIBLE_NODE_ID,
|
|
||||||
type: 'invisible',
|
|
||||||
position: {
|
|
||||||
x: prevInvisibleNode ? prevInvisibleNode.position.x : 0,
|
|
||||||
y: prevInvisibleNode ? prevInvisibleNode.position.y : 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
},
|
|
||||||
[currentStepId, nodes, onStepChange, openNextStep],
|
|
||||||
);
|
|
||||||
|
|
||||||
const updateNodesData = useCallback(
|
|
||||||
(steps) => {
|
|
||||||
setNodes((nodes) =>
|
|
||||||
nodes.map((node) => {
|
|
||||||
const step = steps.find((step) => step.id === node.id);
|
|
||||||
if (step) {
|
|
||||||
return { ...node, data: { ...node.data, step: { ...step } } };
|
|
||||||
}
|
|
||||||
return node;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[setNodes],
|
|
||||||
);
|
|
||||||
|
|
||||||
const updateEdgesData = useCallback(
|
|
||||||
(flow) => {
|
|
||||||
setEdges((edges) =>
|
|
||||||
edges.map((edge) => {
|
|
||||||
return {
|
|
||||||
...edge,
|
|
||||||
data: { ...edge.data, flowId: flow.id, flowActive: flow.active },
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[setEdges],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setNodes(
|
|
||||||
nodes.map((node) => {
|
|
||||||
if (node.type === 'flowStep') {
|
|
||||||
const collapsed = currentStepId !== node.data.step.id;
|
|
||||||
return {
|
|
||||||
...node,
|
|
||||||
zIndex: collapsed ? 0 : 1,
|
|
||||||
data: {
|
|
||||||
...node.data,
|
|
||||||
collapsed,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return node;
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
}, [currentStepId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useAutoLayout();
|
||||||
if (flow.steps.length + 1 !== nodes.length) {
|
|
||||||
const newNodes = generateNodes(flow, nodes);
|
|
||||||
const newEdges = generateEdges(flow, edges);
|
|
||||||
|
|
||||||
setNodes(newNodes);
|
const createdStepIdRef = useRef(null);
|
||||||
setEdges(newEdges);
|
|
||||||
} else {
|
|
||||||
updateNodesData(flow.steps);
|
|
||||||
updateEdgesData(flow);
|
|
||||||
}
|
|
||||||
}, [flow]);
|
|
||||||
|
|
||||||
return (
|
const openNextStep = useCallback(
|
||||||
<EditorWrapper direction="column">
|
(currentStepId) => {
|
||||||
<ReactFlow
|
setNodes((nodes) => {
|
||||||
nodes={nodes}
|
const currentStepIndex = nodes.findIndex(
|
||||||
edges={edges}
|
(node) => node.id === currentStepId,
|
||||||
onNodesChange={onNodesChange}
|
);
|
||||||
onEdgesChange={onEdgesChange}
|
if (currentStepIndex >= 0) {
|
||||||
onConnect={onConnect}
|
const nextStep = nodes[currentStepIndex + 1];
|
||||||
nodeTypes={nodeTypes}
|
return updatedCollapsedNodes(nodes, nextStep.id);
|
||||||
edgeTypes={edgeTypes}
|
}
|
||||||
panOnScroll
|
return nodes;
|
||||||
panOnScrollMode="vertical"
|
});
|
||||||
panOnDrag={false}
|
},
|
||||||
zoomOnScroll={false}
|
[setNodes],
|
||||||
zoomOnPinch={false}
|
);
|
||||||
zoomOnDoubleClick={false}
|
|
||||||
panActivationKeyCode={null}
|
const onStepClose = useCallback(() => {
|
||||||
proOptions={{ hideAttribution: true }}
|
setNodes((nodes) => updatedCollapsedNodes(nodes));
|
||||||
/>
|
}, [setNodes]);
|
||||||
</EditorWrapper>
|
|
||||||
);
|
const onStepOpen = useCallback(
|
||||||
};
|
(stepId) => {
|
||||||
|
setNodes((nodes) => updatedCollapsedNodes(nodes, stepId));
|
||||||
|
},
|
||||||
|
[setNodes],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onStepChange = useCallback(
|
||||||
|
async (step) => {
|
||||||
|
// const mutationInput = {
|
||||||
|
// id: step.id,
|
||||||
|
// key: step.key,
|
||||||
|
// parameters: step.parameters,
|
||||||
|
// connection: {
|
||||||
|
// id: step.connection?.id,
|
||||||
|
// },
|
||||||
|
// flow: {
|
||||||
|
// id: flow.id,
|
||||||
|
// },
|
||||||
|
// };
|
||||||
|
// if (step.appKey) {
|
||||||
|
// mutationInput.appKey = step.appKey;
|
||||||
|
// }
|
||||||
|
// const updated = await updateStep({
|
||||||
|
// variables: { input: mutationInput },
|
||||||
|
// });
|
||||||
|
// await queryClient.invalidateQueries({
|
||||||
|
// queryKey: ['steps', step.id, 'connection'],
|
||||||
|
// });
|
||||||
|
// await queryClient.invalidateQueries({ queryKey: ['flows', flow.id] });
|
||||||
|
},
|
||||||
|
// [flow.id, updateStep, queryClient],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onAddStep = async (previousStepId) => {
|
||||||
|
const createdStepId = createStep(flow, previousStepId);
|
||||||
|
createdStepIdRef.current = createdStepId;
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log({ flow });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// if (flow.steps.length + 1 !== nodes.length) {
|
||||||
|
setNodes((nodes) =>
|
||||||
|
generateNodes({
|
||||||
|
prevNodes: nodes,
|
||||||
|
steps: flow.steps,
|
||||||
|
createdStepId: createdStepIdRef.current,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
setEdges((edges) =>
|
||||||
|
generateEdges({ prevEdges: edges, steps: flow.steps }),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (createdStepIdRef.current) {
|
||||||
|
createdStepIdRef.current = null;
|
||||||
|
}
|
||||||
|
// }
|
||||||
|
}, [flow.steps]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NodesContext.Provider
|
||||||
|
value={{
|
||||||
|
openNextStep,
|
||||||
|
onStepOpen,
|
||||||
|
onStepClose,
|
||||||
|
onStepChange,
|
||||||
|
flowId: flow.id,
|
||||||
|
steps: flow.steps,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<EdgesContext.Provider
|
||||||
|
value={{
|
||||||
|
stepCreationInProgress,
|
||||||
|
onAddStep,
|
||||||
|
onAddPaths: createPaths,
|
||||||
|
onAddPath: createPath,
|
||||||
|
flowActive: flow.active,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<EditorWrapper direction="column">
|
||||||
|
<ReactFlow
|
||||||
|
nodes={nodes}
|
||||||
|
edges={edges}
|
||||||
|
onNodesChange={onNodesChange}
|
||||||
|
onEdgesChange={onEdgesChange}
|
||||||
|
nodeTypes={nodeTypes}
|
||||||
|
edgeTypes={edgeTypes}
|
||||||
|
fitView
|
||||||
|
maxZoom={1}
|
||||||
|
minZoom={0.001}
|
||||||
|
proOptions={{ hideAttribution: true }}
|
||||||
|
/>
|
||||||
|
</EditorWrapper>
|
||||||
|
</EdgesContext.Provider>
|
||||||
|
</NodesContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
EditorNew.propTypes = {
|
EditorNew.propTypes = {
|
||||||
flow: FlowPropType.isRequired,
|
flow: FlowPropType.isRequired,
|
||||||
|
@@ -1,72 +0,0 @@
|
|||||||
import { Handle, Position } from 'reactflow';
|
|
||||||
import { Box } from '@mui/material';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import FlowStep from 'components/FlowStep';
|
|
||||||
import { StepPropType } from 'propTypes/propTypes';
|
|
||||||
|
|
||||||
import { NodeWrapper, NodeInnerWrapper } from './style.js';
|
|
||||||
|
|
||||||
function FlowStepNode({
|
|
||||||
data: {
|
|
||||||
step,
|
|
||||||
index,
|
|
||||||
flowId,
|
|
||||||
collapsed,
|
|
||||||
openNextStep,
|
|
||||||
onOpen,
|
|
||||||
onClose,
|
|
||||||
onChange,
|
|
||||||
layouted,
|
|
||||||
},
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<NodeWrapper
|
|
||||||
className="nodrag"
|
|
||||||
sx={{
|
|
||||||
visibility: layouted ? 'visible' : 'hidden',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<NodeInnerWrapper>
|
|
||||||
<Handle
|
|
||||||
type="target"
|
|
||||||
position={Position.Top}
|
|
||||||
isConnectable={false}
|
|
||||||
style={{ visibility: 'hidden' }}
|
|
||||||
/>
|
|
||||||
<FlowStep
|
|
||||||
step={step}
|
|
||||||
index={index + 1}
|
|
||||||
collapsed={collapsed}
|
|
||||||
onOpen={onOpen}
|
|
||||||
onClose={onClose}
|
|
||||||
onChange={onChange}
|
|
||||||
flowId={flowId}
|
|
||||||
onContinue={openNextStep}
|
|
||||||
/>
|
|
||||||
<Handle
|
|
||||||
type="source"
|
|
||||||
position={Position.Bottom}
|
|
||||||
isConnectable={false}
|
|
||||||
style={{ visibility: 'hidden' }}
|
|
||||||
/>
|
|
||||||
</NodeInnerWrapper>
|
|
||||||
</NodeWrapper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
FlowStepNode.propTypes = {
|
|
||||||
data: PropTypes.shape({
|
|
||||||
step: StepPropType.isRequired,
|
|
||||||
index: PropTypes.number.isRequired,
|
|
||||||
flowId: PropTypes.string.isRequired,
|
|
||||||
collapsed: PropTypes.bool.isRequired,
|
|
||||||
openNextStep: PropTypes.func.isRequired,
|
|
||||||
onOpen: PropTypes.func.isRequired,
|
|
||||||
onClose: PropTypes.func.isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
layouted: PropTypes.bool.isRequired,
|
|
||||||
}).isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FlowStepNode;
|
|
@@ -0,0 +1,69 @@
|
|||||||
|
import { Handle, Position } from 'reactflow';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import FlowStep from 'components/FlowStep';
|
||||||
|
|
||||||
|
import { NodeWrapper, NodeInnerWrapper } from './style.js';
|
||||||
|
import { useContext } from 'react';
|
||||||
|
import { NodesContext } from '../../EditorNew.jsx';
|
||||||
|
import { findStepByStepId } from 'components/EditorNew/utils.js';
|
||||||
|
|
||||||
|
function FlowStepNode({ data: { collapsed, laidOut }, id }) {
|
||||||
|
const { openNextStep, onStepOpen, onStepClose, onStepChange, flowId, steps } =
|
||||||
|
useContext(NodesContext);
|
||||||
|
|
||||||
|
const step = findStepByStepId({ steps }, id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
// <NodeWrapper
|
||||||
|
// sx={{
|
||||||
|
// visibility: laidOut ? 'visible' : 'hidden',
|
||||||
|
// }}
|
||||||
|
// >
|
||||||
|
<NodeInnerWrapper
|
||||||
|
sx={
|
||||||
|
{
|
||||||
|
// visibility: laidOut ? 'visible' : 'hidden',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
id="flowStepId"
|
||||||
|
className="nodrag"
|
||||||
|
>
|
||||||
|
<Handle
|
||||||
|
type="target"
|
||||||
|
position={Position.Top}
|
||||||
|
isConnectable={false}
|
||||||
|
style={{ visibility: 'hidden' }}
|
||||||
|
/>
|
||||||
|
{step && (
|
||||||
|
<FlowStep
|
||||||
|
step={step}
|
||||||
|
collapsed={collapsed}
|
||||||
|
onOpen={() => onStepOpen(step.id)}
|
||||||
|
onClose={onStepClose}
|
||||||
|
onChange={onStepChange}
|
||||||
|
flowId={flowId}
|
||||||
|
onContinue={() => openNextStep(step.id)}
|
||||||
|
collapseAnimation={false}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Handle
|
||||||
|
type="source"
|
||||||
|
position={Position.Bottom}
|
||||||
|
isConnectable={false}
|
||||||
|
style={{ visibility: 'hidden' }}
|
||||||
|
/>
|
||||||
|
</NodeInnerWrapper>
|
||||||
|
// </NodeWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
FlowStepNode.propTypes = {
|
||||||
|
id: PropTypes.string,
|
||||||
|
data: PropTypes.shape({
|
||||||
|
collapsed: PropTypes.bool.isRequired,
|
||||||
|
laidOut: PropTypes.bool.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FlowStepNode;
|
@@ -9,6 +9,6 @@ export const NodeWrapper = styled(Box)(({ theme }) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
export const NodeInnerWrapper = styled(Box)(({ theme }) => ({
|
export const NodeInnerWrapper = styled(Box)(({ theme }) => ({
|
||||||
maxWidth: 900,
|
width: 900,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
}));
|
}));
|
@@ -0,0 +1,98 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Handle, Position } from 'reactflow';
|
||||||
|
import { Box, Stack, Typography } from '@mui/material';
|
||||||
|
import { useRef, useState } from 'react';
|
||||||
|
import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
|
||||||
|
import Menu from '@mui/material/Menu';
|
||||||
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
|
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
import { Wrapper } from './style';
|
||||||
|
|
||||||
|
/* TODO
|
||||||
|
- add delete
|
||||||
|
- add rename
|
||||||
|
- add translations
|
||||||
|
- add collapsing?
|
||||||
|
*/
|
||||||
|
|
||||||
|
function PathNode({ data: { laidOut } }) {
|
||||||
|
const [anchorEl, setAnchorEl] = useState(null);
|
||||||
|
const contextButtonRef = useRef(null);
|
||||||
|
|
||||||
|
const onContextMenuClose = (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
setAnchorEl(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onContextMenuClick = (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
setAnchorEl(contextButtonRef.current);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deletePath = () => {
|
||||||
|
setAnchorEl(null);
|
||||||
|
onContextMenuClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Box
|
||||||
|
className="nodrag"
|
||||||
|
sx={
|
||||||
|
{
|
||||||
|
// visibility: laidOut ? 'visible' : 'hidden',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Handle
|
||||||
|
type="target"
|
||||||
|
position={Position.Top}
|
||||||
|
isConnectable={false}
|
||||||
|
style={{ visibility: 'hidden' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Wrapper>
|
||||||
|
<Stack
|
||||||
|
justifyContent="space-between"
|
||||||
|
alignItems="center"
|
||||||
|
direction="row"
|
||||||
|
>
|
||||||
|
<Typography sx={{ pr: 2 }}>Path</Typography>
|
||||||
|
<IconButton
|
||||||
|
color="primary"
|
||||||
|
onClick={onContextMenuClick}
|
||||||
|
ref={contextButtonRef}
|
||||||
|
>
|
||||||
|
<MoreHorizIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Stack>
|
||||||
|
</Wrapper>
|
||||||
|
<Handle
|
||||||
|
type="source"
|
||||||
|
position={Position.Bottom}
|
||||||
|
isConnectable={false}
|
||||||
|
style={{ visibility: 'hidden' }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
{anchorEl && (
|
||||||
|
<Menu
|
||||||
|
open={true}
|
||||||
|
onClose={onContextMenuClose}
|
||||||
|
hideBackdrop={false}
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
>
|
||||||
|
<MenuItem onClick={deletePath}>Delete</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
PathNode.propTypes = {
|
||||||
|
data: PropTypes.shape({
|
||||||
|
laidOut: PropTypes.bool,
|
||||||
|
}).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PathNode;
|
@@ -0,0 +1,8 @@
|
|||||||
|
import { styled } from '@mui/material/styles';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
|
||||||
|
export const Wrapper = styled(Box)(({ theme }) => ({
|
||||||
|
padding: theme.spacing(1, 2),
|
||||||
|
backgroundColor: '#0059f714',
|
||||||
|
borderRadius: 20,
|
||||||
|
}));
|
@@ -0,0 +1,108 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Handle, Position } from 'reactflow';
|
||||||
|
import { Avatar, Box, Stack, Typography } from '@mui/material';
|
||||||
|
import { useRef, useState } from 'react';
|
||||||
|
import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
|
||||||
|
import Menu from '@mui/material/Menu';
|
||||||
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
|
import CallSplitIcon from '@mui/icons-material/CallSplit';
|
||||||
|
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
import { Wrapper } from './style';
|
||||||
|
|
||||||
|
/* TODO
|
||||||
|
- add delete
|
||||||
|
- add rename
|
||||||
|
- add translations
|
||||||
|
- add collapsing?
|
||||||
|
*/
|
||||||
|
|
||||||
|
function PathsNode({ data: { laidOut } }) {
|
||||||
|
const [anchorEl, setAnchorEl] = useState(null);
|
||||||
|
const contextButtonRef = useRef(null);
|
||||||
|
|
||||||
|
const onContextMenuClose = (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
setAnchorEl(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onContextMenuClick = (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
setAnchorEl(contextButtonRef.current);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deletePaths = () => {
|
||||||
|
setAnchorEl(null);
|
||||||
|
onContextMenuClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Box
|
||||||
|
width={900}
|
||||||
|
className="nodrag"
|
||||||
|
sx={
|
||||||
|
{
|
||||||
|
// visibility: laidOut ? 'visible' : 'hidden',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Handle
|
||||||
|
type="target"
|
||||||
|
position={Position.Top}
|
||||||
|
isConnectable={false}
|
||||||
|
style={{ visibility: 'hidden' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Wrapper>
|
||||||
|
<Stack justifyContent="space-between" direction="row">
|
||||||
|
<Stack direction="row" alignItems="center" spacing={2}>
|
||||||
|
<Avatar
|
||||||
|
sx={{ display: 'flex', width: 50, height: 50 }}
|
||||||
|
variant="square"
|
||||||
|
>
|
||||||
|
<CallSplitIcon
|
||||||
|
fontSize="large"
|
||||||
|
sx={{ transform: 'rotate(180deg)' }}
|
||||||
|
/>
|
||||||
|
</Avatar>
|
||||||
|
{/* TODO name from path data */}
|
||||||
|
<Typography>Paths</Typography>
|
||||||
|
</Stack>
|
||||||
|
<IconButton
|
||||||
|
color="primary"
|
||||||
|
onClick={onContextMenuClick}
|
||||||
|
ref={contextButtonRef}
|
||||||
|
>
|
||||||
|
<MoreHorizIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Stack>
|
||||||
|
</Wrapper>
|
||||||
|
<Handle
|
||||||
|
type="source"
|
||||||
|
position={Position.Bottom}
|
||||||
|
isConnectable={false}
|
||||||
|
style={{ visibility: 'hidden' }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
{anchorEl && (
|
||||||
|
<Menu
|
||||||
|
open={true}
|
||||||
|
onClose={onContextMenuClose}
|
||||||
|
hideBackdrop={false}
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
>
|
||||||
|
<MenuItem onClick={deletePaths}>Delete</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
PathsNode.propTypes = {
|
||||||
|
data: PropTypes.shape({
|
||||||
|
laidOut: PropTypes.bool,
|
||||||
|
}).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PathsNode;
|
@@ -0,0 +1,7 @@
|
|||||||
|
import { styled } from '@mui/material/styles';
|
||||||
|
import Card from '@mui/material/Card';
|
||||||
|
|
||||||
|
export const Wrapper = styled(Card)`
|
||||||
|
width: 100%;
|
||||||
|
padding: ${({ theme }) => theme.spacing(2)};
|
||||||
|
`;
|
14
packages/web/src/components/EditorNew/constants.js
Normal file
14
packages/web/src/components/EditorNew/constants.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
export const INVISIBLE_NODE_ID = 'invisible-node';
|
||||||
|
|
||||||
|
export const NODE_TYPES = {
|
||||||
|
FLOW_STEP: 'flowStep',
|
||||||
|
INVISIBLE: 'invisible',
|
||||||
|
PATHS: 'parallelPaths',
|
||||||
|
PATH: 'path',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EDGE_TYPES = {
|
||||||
|
ADD_NODE_OR_PATHS_EDGE: 'addNodeOrPathsEdge',
|
||||||
|
ADD_PATH_EDGE: 'addPathEdge',
|
||||||
|
ADD_NODE_EDGE: 'addNodeEdge',
|
||||||
|
};
|
@@ -7,7 +7,7 @@ export const EditorWrapper = styled(Stack)(({ theme }) => ({
|
|||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
},
|
},
|
||||||
|
|
||||||
'& .react-flow__pane, & .react-flow__node': {
|
// '& .react-flow__pane, & .react-flow__node': {
|
||||||
cursor: 'auto !important',
|
// cursor: 'auto !important',
|
||||||
},
|
// },
|
||||||
}));
|
}));
|
||||||
|
96
packages/web/src/components/EditorNew/temp/useFlow.js
Normal file
96
packages/web/src/components/EditorNew/temp/useFlow.js
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import { insertStep } from '../utils';
|
||||||
|
|
||||||
|
const initFlow = {
|
||||||
|
id: '7c55e6ce-a84a-46e3-ba31-211ec7b5c2cb',
|
||||||
|
name: 'Name your flow',
|
||||||
|
active: false,
|
||||||
|
status: 'draft',
|
||||||
|
createdAt: 1718264916266,
|
||||||
|
updatedAt: 1718264916266,
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
id: '82ce34ab-7aab-4e6c-9f62-db5104aa81c6',
|
||||||
|
type: 'trigger',
|
||||||
|
key: null,
|
||||||
|
appKey: null,
|
||||||
|
iconUrl: null,
|
||||||
|
webhookUrl: 'http://localhost:3000/null',
|
||||||
|
status: 'incomplete',
|
||||||
|
position: 1,
|
||||||
|
parameters: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '41c60527-eb4f-4f2d-93ec-2fd37e336909',
|
||||||
|
type: 'action',
|
||||||
|
key: null,
|
||||||
|
appKey: null,
|
||||||
|
iconUrl: null,
|
||||||
|
webhookUrl: 'http://localhost:3000/null',
|
||||||
|
status: 'incomplete',
|
||||||
|
position: 2,
|
||||||
|
parameters: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateStep = () => {
|
||||||
|
return {
|
||||||
|
id: uuidv4(),
|
||||||
|
type: 'action',
|
||||||
|
key: null,
|
||||||
|
appKey: null,
|
||||||
|
parameters: {},
|
||||||
|
iconUrl: null,
|
||||||
|
webhookUrl: 'http://localhost:3000/null',
|
||||||
|
status: 'incomplete',
|
||||||
|
connection: null,
|
||||||
|
position: null,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const generatePath = (steps) => {
|
||||||
|
return {
|
||||||
|
id: uuidv4(),
|
||||||
|
type: 'path',
|
||||||
|
steps: steps?.length > 0 ? steps : [generateStep()],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generatePaths = (steps) => {
|
||||||
|
return {
|
||||||
|
id: uuidv4(),
|
||||||
|
type: 'parallelPaths',
|
||||||
|
steps: [generatePath(steps), generatePath()],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useFlow = () => {
|
||||||
|
const [flow, setFlow] = useState(initFlow);
|
||||||
|
|
||||||
|
const createStep = (flow, previousStepId) => {
|
||||||
|
const newStep = generateStep();
|
||||||
|
const newFlow = insertStep(flow, previousStepId, newStep);
|
||||||
|
|
||||||
|
setFlow(newFlow);
|
||||||
|
return newStep.id;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createPaths = (previousStepId) => {
|
||||||
|
const newFlow = insertStep(flow, previousStepId, generatePaths());
|
||||||
|
setFlow(newFlow);
|
||||||
|
};
|
||||||
|
|
||||||
|
const createPath = (previousStepId) => {
|
||||||
|
const newFlow = insertStep(flow, previousStepId, generatePath());
|
||||||
|
setFlow(newFlow);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
flow,
|
||||||
|
createStep,
|
||||||
|
createPaths,
|
||||||
|
createPath,
|
||||||
|
};
|
||||||
|
};
|
@@ -4,7 +4,7 @@ import { usePrevious } from 'hooks/usePrevious';
|
|||||||
import { isEqual } from 'lodash';
|
import { isEqual } from 'lodash';
|
||||||
import { useNodesInitialized, useNodes, useReactFlow } from 'reactflow';
|
import { useNodesInitialized, useNodes, useReactFlow } from 'reactflow';
|
||||||
|
|
||||||
const getLayoutedElements = (nodes, edges) => {
|
const getLaidOutElements = (nodes, edges) => {
|
||||||
const graph = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
|
const graph = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
|
||||||
graph.setGraph({
|
graph.setGraph({
|
||||||
rankdir: 'TB',
|
rankdir: 'TB',
|
||||||
@@ -32,22 +32,22 @@ export const useAutoLayout = () => {
|
|||||||
const nodes = useNodes();
|
const nodes = useNodes();
|
||||||
const prevNodes = usePrevious(nodes);
|
const prevNodes = usePrevious(nodes);
|
||||||
const nodesInitialized = useNodesInitialized();
|
const nodesInitialized = useNodesInitialized();
|
||||||
const { getEdges, setNodes, setEdges } = useReactFlow();
|
const { getEdges, setNodes, setEdges, fitView } = useReactFlow();
|
||||||
|
|
||||||
const onLayout = useCallback(
|
const onLayout = useCallback(
|
||||||
(nodes, edges) => {
|
(nodes, edges) => {
|
||||||
const layoutedElements = getLayoutedElements(nodes, edges);
|
const laidOutElements = getLaidOutElements(nodes, edges);
|
||||||
|
|
||||||
setNodes([
|
setNodes([
|
||||||
...layoutedElements.nodes.map((node) => ({
|
...laidOutElements.nodes.map((node) => ({
|
||||||
...node,
|
...node,
|
||||||
data: { ...node.data, layouted: true },
|
data: { ...node.data, laidOut: true },
|
||||||
})),
|
})),
|
||||||
]);
|
]);
|
||||||
setEdges([
|
setEdges([
|
||||||
...layoutedElements.edges.map((edge) => ({
|
...laidOutElements.edges.map((edge) => ({
|
||||||
...edge,
|
...edge,
|
||||||
data: { ...edge.data, layouted: true },
|
data: { ...edge.data, laidOut: true },
|
||||||
})),
|
})),
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
@@ -62,6 +62,8 @@ export const useAutoLayout = () => {
|
|||||||
prevNodes.map(({ width, height }) => ({ width, height })),
|
prevNodes.map(({ width, height }) => ({ width, height })),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
fitView();
|
||||||
|
|
||||||
if (shouldAutoLayout) {
|
if (shouldAutoLayout) {
|
||||||
onLayout(nodes, getEdges());
|
onLayout(nodes, getEdges());
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useViewport, useReactFlow } from 'reactflow';
|
import { useViewport, useReactFlow } from 'reactflow';
|
||||||
|
|
||||||
export const useScrollBoundries = () => {
|
export const useScrollBoundaries = () => {
|
||||||
const { setViewport } = useReactFlow();
|
const { setViewport } = useReactFlow();
|
||||||
const { x, y, zoom } = useViewport();
|
const { x, y, zoom } = useViewport();
|
||||||
|
|
245
packages/web/src/components/EditorNew/utils.js
Normal file
245
packages/web/src/components/EditorNew/utils.js
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
import cloneDeep from 'lodash/cloneDeep.js';
|
||||||
|
import { EDGE_TYPES, INVISIBLE_NODE_ID, NODE_TYPES } from './constants';
|
||||||
|
|
||||||
|
export const generateEdgeId = (sourceId, targetId) =>
|
||||||
|
`${sourceId}--${targetId}`;
|
||||||
|
|
||||||
|
export const updatedCollapsedNodes = (nodes, openStepId) => {
|
||||||
|
return nodes.map((node) => {
|
||||||
|
if (node.type !== NODE_TYPES.FLOW_STEP) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
const collapsed = node.id !== openStepId;
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
zIndex: collapsed ? 0 : 1,
|
||||||
|
data: { ...node.data, collapsed },
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateNodes = ({ steps, prevNodes, createdStepId }) => {
|
||||||
|
const newNodes = steps.map((step, index) => {
|
||||||
|
const collapsed = index !== 0;
|
||||||
|
|
||||||
|
const prevNode = prevNodes?.find(({ id }) => id === step.id);
|
||||||
|
|
||||||
|
let newNode;
|
||||||
|
let childSteps = [];
|
||||||
|
|
||||||
|
switch (step.type) {
|
||||||
|
case 'trigger':
|
||||||
|
case 'action': {
|
||||||
|
if (prevNode) {
|
||||||
|
newNode = {
|
||||||
|
...prevNode,
|
||||||
|
zIndex: createdStepId ? 0 : prevNode?.zIndex || 0,
|
||||||
|
data: {
|
||||||
|
...prevNode.data,
|
||||||
|
collapsed: createdStepId
|
||||||
|
? true
|
||||||
|
: prevNode?.data?.collapsed || true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
newNode = {
|
||||||
|
id: step.id,
|
||||||
|
type: NODE_TYPES.FLOW_STEP,
|
||||||
|
position: {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
zIndex: collapsed ? 0 : 1,
|
||||||
|
data: {
|
||||||
|
collapsed,
|
||||||
|
laidOut: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'parallelPaths': {
|
||||||
|
if (prevNode) {
|
||||||
|
newNode = prevNode;
|
||||||
|
} else {
|
||||||
|
newNode = {
|
||||||
|
id: step.id,
|
||||||
|
type: NODE_TYPES.PATHS,
|
||||||
|
position: {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
laidOut: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'path': {
|
||||||
|
if (prevNode) {
|
||||||
|
newNode = prevNode;
|
||||||
|
} else {
|
||||||
|
newNode = {
|
||||||
|
id: step.id,
|
||||||
|
type: NODE_TYPES.PATH,
|
||||||
|
position: {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
laidOut: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
if (step?.steps?.length > 0) {
|
||||||
|
childSteps = generateNodes({
|
||||||
|
steps: step.steps,
|
||||||
|
prevNodes,
|
||||||
|
createdStepId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return [newNode, ...childSteps];
|
||||||
|
});
|
||||||
|
|
||||||
|
return newNodes.flat(Infinity);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateEdges = ({ steps }) => {
|
||||||
|
const newEdges = steps.map((step, index) => {
|
||||||
|
switch (step.type) {
|
||||||
|
case 'parallelPaths': {
|
||||||
|
const edges = step.steps.map((childStep) => {
|
||||||
|
const sourceId = step.id;
|
||||||
|
const targetId = childStep.id;
|
||||||
|
|
||||||
|
const newEdge = {
|
||||||
|
id: generateEdgeId(sourceId, targetId),
|
||||||
|
source: sourceId,
|
||||||
|
target: targetId,
|
||||||
|
type: EDGE_TYPES.ADD_PATH_EDGE,
|
||||||
|
data: {
|
||||||
|
laidOut: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return newEdge;
|
||||||
|
});
|
||||||
|
|
||||||
|
const childEdges = generateEdges({ steps: step.steps });
|
||||||
|
|
||||||
|
return [...edges, ...childEdges];
|
||||||
|
}
|
||||||
|
case 'path': {
|
||||||
|
console.log({ step });
|
||||||
|
|
||||||
|
const sourceId = step.id;
|
||||||
|
const targetId = step.steps?.[0]?.id;
|
||||||
|
|
||||||
|
if (targetId) {
|
||||||
|
const newEdge = {
|
||||||
|
id: generateEdgeId(sourceId, targetId),
|
||||||
|
source: sourceId,
|
||||||
|
target: targetId,
|
||||||
|
type: EDGE_TYPES.ADD_NODE_EDGE,
|
||||||
|
data: {
|
||||||
|
laidOut: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const childEdges = generateEdges({ steps: step.steps });
|
||||||
|
|
||||||
|
return [newEdge, ...childEdges];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
const sourceId = step.id;
|
||||||
|
const targetId = steps[index + 1]?.id;
|
||||||
|
|
||||||
|
if (targetId) {
|
||||||
|
return {
|
||||||
|
id: generateEdgeId(sourceId, targetId),
|
||||||
|
source: sourceId,
|
||||||
|
target: targetId,
|
||||||
|
type: EDGE_TYPES.ADD_NODE_OR_PATHS_EDGE,
|
||||||
|
data: {
|
||||||
|
laidOut: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return newEdges.flat(Infinity).filter((edge) => !!edge);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const findStepByStepId = (obj, id) => {
|
||||||
|
if (Array.isArray(obj.steps)) {
|
||||||
|
for (const step of obj.steps) {
|
||||||
|
if (step.id === id) {
|
||||||
|
return step;
|
||||||
|
}
|
||||||
|
const result = findStepByStepId(step, id);
|
||||||
|
if (result) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function insertStep(parentObj, id, newStep) {
|
||||||
|
function recursiveFindAndInsert(parentObj, id, newStep) {
|
||||||
|
if (parentObj.steps && Array.isArray(parentObj.steps)) {
|
||||||
|
for (let index = 0; index < parentObj.steps.length; index++) {
|
||||||
|
const step = parentObj.steps[index];
|
||||||
|
if (step.id === id) {
|
||||||
|
if (newStep.type === NODE_TYPES.PATHS) {
|
||||||
|
const stepsAfter = parentObj.steps.slice(
|
||||||
|
index + 1,
|
||||||
|
parentObj.steps.length,
|
||||||
|
);
|
||||||
|
parentObj.steps.splice(index + 1);
|
||||||
|
newStep.steps[0].steps = stepsAfter;
|
||||||
|
parentObj.steps.splice(index + 1, 0, newStep);
|
||||||
|
} else if (step.type === NODE_TYPES.PATHS) {
|
||||||
|
step.steps.push(newStep);
|
||||||
|
} else if (step.type === NODE_TYPES.PATH) {
|
||||||
|
step.steps.unshift(newStep);
|
||||||
|
} else {
|
||||||
|
const originalSteps = step.steps || [];
|
||||||
|
step.steps = [];
|
||||||
|
|
||||||
|
const newStepObject = {
|
||||||
|
...newStep,
|
||||||
|
steps: originalSteps,
|
||||||
|
};
|
||||||
|
|
||||||
|
parentObj.steps.splice(index + 1, 0, newStepObject);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const found = recursiveFindAndInsert(step, id, newStep);
|
||||||
|
if (found) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone the input object to avoid mutating the original
|
||||||
|
const newParentObj = cloneDeep(parentObj);
|
||||||
|
recursiveFindAndInsert(newParentObj, id, newStep);
|
||||||
|
return newParentObj;
|
||||||
|
}
|
@@ -105,7 +105,7 @@ function generateValidationSchema(substeps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function FlowStep(props) {
|
function FlowStep(props) {
|
||||||
const { collapsed, onChange, onContinue, flowId } = props;
|
const { collapsed, onChange, onContinue, flowId, collapseAnimation } = props;
|
||||||
const editorContext = React.useContext(EditorContext);
|
const editorContext = React.useContext(EditorContext);
|
||||||
const contextButtonRef = React.useRef(null);
|
const contextButtonRef = React.useRef(null);
|
||||||
const step = props.step;
|
const step = props.step;
|
||||||
@@ -259,7 +259,11 @@ function FlowStep(props) {
|
|||||||
</Stack>
|
</Stack>
|
||||||
</Header>
|
</Header>
|
||||||
|
|
||||||
<Collapse in={!collapsed} unmountOnExit>
|
<Collapse
|
||||||
|
in={!collapsed}
|
||||||
|
unmountOnExit
|
||||||
|
{...(!collapseAnimation ? { timeout: 0 } : {})}
|
||||||
|
>
|
||||||
<Content>
|
<Content>
|
||||||
<List>
|
<List>
|
||||||
<StepExecutionsProvider value={stepWithTestExecutionsData}>
|
<StepExecutionsProvider value={stepWithTestExecutionsData}>
|
||||||
@@ -360,11 +364,15 @@ 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,
|
||||||
onContinue: PropTypes.func,
|
onContinue: PropTypes.func,
|
||||||
|
collapseAnimation: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
FlowStep.defaultProps = {
|
||||||
|
collapseAnimation: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default FlowStep;
|
export default FlowStep;
|
||||||
|
@@ -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 &&
|
||||||
|
Reference in New Issue
Block a user